Procházet zdrojové kódy

feat(docs): add MongoDB removal feasibility and project state summary documentation

- Introduced a comprehensive document outlining the feasibility of removing MongoDB and MongoStats, detailing usage inventory, data types, minimal MySQL schema proposals, migration plans, and testing checklists.
- Added a project state summary that provides an overview of the project structure, implemented applications, modules, infrastructure, APIs, and known constraints, along with a resume checklist for ongoing development.
Dave před 3 měsíci
rodič
revize
07c04665c6

+ 30 - 0
.env.docker

@@ -0,0 +1,30 @@
+# Shared application configuration for the docker-compose stack
+APP_HOST=0.0.0.0
+APP_CORS_ORIGIN=*
+
+MYSQL_URL="mysql://root:rootpass@box-mysql:3306/box_admin"
+
+MONGO_URL="mongodb://boxadmin:boxpass@box-mongodb:27017/box_admin?replicaSet=rs0&authSource=admin"
+MONGO_STATS_URL="mongodb://boxadmin:boxpass@box-mongodb:27017/box_stats?replicaSet=rs0&authSource=admin"
+
+JWT_SECRET=047df8aaa3d17dc1173c5a9a3052ba66c2b0bd96937147eb643319a0c90d132f
+
+REDIS_HOST=box-redis
+REDIS_PORT=6379
+REDIS_PASSWORD=
+REDIS_KEY_PREFIX=box:
+
+RABBITMQ_URL="amqp://boxrabbit:BoxRabbit2025@box-rabbitmq:5672"
+RABBITMQ_LOGIN_EXCHANGE=stats.user
+RABBITMQ_LOGIN_QUEUE=stats.user.login.q
+RABBITMQ_LOGIN_ROUTING_KEY=user.login
+RABBITMQ_ADS_CLICK_ROUTING_KEY=stats.ad.click
+RABBITMQ_STATS_EXCHANGE=stats.user
+RABBITMQ_STATS_AD_CLICK_QUEUE=stats.ad.click
+RABBITMQ_STATS_VIDEO_CLICK_QUEUE=stats.video.click
+RABBITMQ_STATS_AD_IMPRESSION_QUEUE=stats.ad.impression
+RABBITMQ_STATS_AD_CLICK_ROUTING_KEY=stats.ad.click
+RABBITMQ_STATS_VIDEO_CLICK_ROUTING_KEY=stats.video.click
+RABBITMQ_STATS_AD_IMPRESSION_ROUTING_KEY=stats.ad.impression
+RABBITMQ_DLQ_EXCHANGE=dlq.stats
+RABBITMQ_FALLBACK_REPLAY_ENABLED=false

+ 39 - 0
apps/box-app-api/Dockerfile

@@ -0,0 +1,39 @@
+# syntax=docker/dockerfile:1
+FROM node:20-alpine AS deps
+WORKDIR /workspace
+COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
+COPY tsconfig.json tsconfig.base.json tsconfig.seed.json nest-cli.json ./
+RUN corepack enable && corepack pnpm install --frozen-lockfile
+
+FROM node:20-alpine AS builder
+WORKDIR /workspace
+COPY --from=deps /workspace/node_modules ./node_modules
+COPY --from=deps /workspace/package.json ./package.json
+COPY --from=deps /workspace/pnpm-lock.yaml ./pnpm-lock.yaml
+COPY --from=deps /workspace/pnpm-workspace.yaml ./pnpm-workspace.yaml
+COPY --from=deps /workspace/tsconfig.json ./tsconfig.json
+COPY --from=deps /workspace/tsconfig.base.json ./tsconfig.base.json
+COPY --from=deps /workspace/tsconfig.seed.json ./tsconfig.seed.json
+COPY --from=deps /workspace/nest-cli.json ./nest-cli.json
+COPY apps ./apps
+COPY libs ./libs
+COPY prisma ./prisma
+COPY .env.docker .env.docker
+RUN set -a \
+  && . .env.docker \
+  && corepack enable \
+  && corepack pnpm prisma generate --schema=prisma/mysql/schema/main.prisma \
+  && corepack pnpm prisma generate --schema=prisma/mongo/schema/main.prisma \
+  && corepack pnpm prisma generate --schema=prisma/mongo-stats/schema/main.prisma \
+  && set +a \
+  && corepack pnpm build:app
+
+FROM node:20-alpine AS runner
+WORKDIR /workspace
+COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
+RUN corepack enable && corepack pnpm install --frozen-lockfile --prod
+COPY --from=builder /workspace/dist ./dist
+COPY --from=builder /workspace/node_modules/@prisma ./node_modules/@prisma
+ENV NODE_ENV=production
+EXPOSE 3301
+CMD ["node", "dist/apps/box-app-api/src/main.js"]

+ 39 - 0
apps/box-mgnt-api/Dockerfile

@@ -0,0 +1,39 @@
+# syntax=docker/dockerfile:1
+FROM node:20-alpine AS deps
+WORKDIR /workspace
+COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
+COPY tsconfig.json tsconfig.base.json tsconfig.seed.json nest-cli.json ./
+RUN corepack enable && corepack pnpm install --frozen-lockfile
+
+FROM node:20-alpine AS builder
+WORKDIR /workspace
+COPY --from=deps /workspace/node_modules ./node_modules
+COPY --from=deps /workspace/package.json ./package.json
+COPY --from=deps /workspace/pnpm-lock.yaml ./pnpm-lock.yaml
+COPY --from=deps /workspace/pnpm-workspace.yaml ./pnpm-workspace.yaml
+COPY --from=deps /workspace/tsconfig.json ./tsconfig.json
+COPY --from=deps /workspace/tsconfig.base.json ./tsconfig.base.json
+COPY --from=deps /workspace/tsconfig.seed.json ./tsconfig.seed.json
+COPY --from=deps /workspace/nest-cli.json ./nest-cli.json
+COPY apps ./apps
+COPY libs ./libs
+COPY prisma ./prisma
+COPY .env.docker .env.docker
+RUN set -a \
+  && . .env.docker \
+  && corepack enable \
+  && corepack pnpm prisma generate --schema=prisma/mysql/schema/main.prisma \
+  && corepack pnpm prisma generate --schema=prisma/mongo/schema/main.prisma \
+  && corepack pnpm prisma generate --schema=prisma/mongo-stats/schema/main.prisma \
+  && set +a \
+  && corepack pnpm build:mgnt
+
+FROM node:20-alpine AS runner
+WORKDIR /workspace
+COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
+RUN corepack enable && corepack pnpm install --frozen-lockfile --prod
+COPY --from=builder /workspace/dist ./dist
+COPY --from=builder /workspace/node_modules/@prisma ./node_modules/@prisma
+ENV NODE_ENV=production
+EXPOSE 3300
+CMD ["node", "dist/apps/box-mgnt-api/src/main.js"]

+ 39 - 0
apps/box-stats-api/Dockerfile

@@ -0,0 +1,39 @@
+# syntax=docker/dockerfile:1
+FROM node:20-alpine AS deps
+WORKDIR /workspace
+COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
+COPY tsconfig.json tsconfig.base.json tsconfig.seed.json nest-cli.json ./
+RUN corepack enable && corepack pnpm install --frozen-lockfile
+
+FROM node:20-alpine AS builder
+WORKDIR /workspace
+COPY --from=deps /workspace/node_modules ./node_modules
+COPY --from=deps /workspace/package.json ./package.json
+COPY --from=deps /workspace/pnpm-lock.yaml ./pnpm-lock.yaml
+COPY --from=deps /workspace/pnpm-workspace.yaml ./pnpm-workspace.yaml
+COPY --from=deps /workspace/tsconfig.json ./tsconfig.json
+COPY --from=deps /workspace/tsconfig.base.json ./tsconfig.base.json
+COPY --from=deps /workspace/tsconfig.seed.json ./tsconfig.seed.json
+COPY --from=deps /workspace/nest-cli.json ./nest-cli.json
+COPY apps ./apps
+COPY libs ./libs
+COPY prisma ./prisma
+COPY .env.docker .env.docker
+RUN set -a \
+  && . .env.docker \
+  && corepack enable \
+  && corepack pnpm prisma generate --schema=prisma/mysql/schema/main.prisma \
+  && corepack pnpm prisma generate --schema=prisma/mongo/schema/main.prisma \
+  && corepack pnpm prisma generate --schema=prisma/mongo-stats/schema/main.prisma \
+  && set +a \
+  && corepack pnpm build:stats
+
+FROM node:20-alpine AS runner
+WORKDIR /workspace
+COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
+RUN corepack enable && corepack pnpm install --frozen-lockfile --prod
+COPY --from=builder /workspace/dist ./dist
+COPY --from=builder /workspace/node_modules/@prisma ./node_modules/@prisma
+ENV NODE_ENV=production
+EXPOSE 3302
+CMD ["node", "dist/apps/box-stats-api/src/main.js"]

+ 14 - 0
box-mgnt-note.md

@@ -46,6 +46,20 @@ docker run -d \
 
 ```
 
+Docker Compose now brings up Redis, RabbitMQ, MongoDB (with a single-node replica set), MySQL, and all three APIs with multi-stage builds, shared networking, healthchecks, and service-healthy dependencies so docker compose up -d gives you the full stack in one go.
+
+Compose orchestrates Redis, RabbitMQ, Mongo, MySQL, and the mgnt/app/stats builds with named volumes, healthchecks, env_file wiring, single box-network, and service_healthy dependencies so each API waits for infra services (docker-compose.yml (lines 3-163)).
+Dockerfiles for each API reuse the workspace cache, run the respective pnpm build:\* target, and ship only the production runtime to keep images small (Dockerfile (lines 1-30), Dockerfile (lines 1-30), Dockerfile (lines 1-30)).
+.env.docker centralizes database, Redis, and RabbitMQ credentials/routing so the APIs stay consistent while allowing per-service overrides (ports, Redis DB, TLS) in Compose (.env.docker (lines 1-30)).
+Mongo init script starts mongod --replSet rs0 --bind_ip_all and ensures rs0 is initiated only once before touching the box_admin and box_stats logical databases (init-repl.js (lines 1-56)).
+Docs explain the one-command workflow, helper commands, and stack highlights for any teammate who needs to get the stack running (DOCKER.md (lines 1-30)).
+Tests: Not run (Docker stack not started).
+
+Next steps:
+
+Run docker compose up -d (or docker compose up -d --build after code changes) to verify the stack builds and services become healthy.
+Use docker compose exec box-mongodb mongosh --eval 'rs.status()' or docker compose logs -f box-mgnt-api if you need to inspect the replica set or API startup.
+
 ```markdown
 # RabbitMQ Management UI Access
 

+ 163 - 0
docker-compose.yml

@@ -0,0 +1,163 @@
+version: '3.9'
+
+services:
+  box-redis:
+    image: redis:7
+    restart: unless-stopped
+    ports:
+      - '6379:6379'
+    healthcheck:
+      test: ["CMD", "redis-cli", "-h", "127.0.0.1", "ping"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+    volumes:
+      - redis_data:/data
+    networks:
+      - box-network
+
+  box-rabbitmq:
+    image: rabbitmq:3.12-management
+    restart: unless-stopped
+    ports:
+      - '5672:5672'
+      - '15672:15672'
+    environment:
+      RABBITMQ_DEFAULT_USER: boxrabbit
+      RABBITMQ_DEFAULT_PASS: BoxRabbit2025
+      RABBITMQ_DEFAULT_VHOST: /
+    healthcheck:
+      test: ["CMD", "rabbitmq-diagnostics", "status"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+    volumes:
+      - rabbitmq_data:/var/lib/rabbitmq
+    networks:
+      - box-network
+
+  box-mongodb:
+    image: mongo:7
+    restart: unless-stopped
+    command: ["mongod", "--replSet", "rs0", "--bind_ip_all"]
+    ports:
+      - '27017:27017'
+    environment:
+      MONGO_INITDB_ROOT_USERNAME: boxadmin
+      MONGO_INITDB_ROOT_PASSWORD: boxpass
+      MONGO_INITDB_DATABASE: box_admin
+    healthcheck:
+      test: ["CMD", "mongo", "--quiet", "--eval", "db.adminCommand('ping')", "-u", "boxadmin", "-p", "boxpass", "--authenticationDatabase", "admin"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+    volumes:
+      - mongo_data:/data/db
+      - ./docker/mongo/init-repl.js:/docker-entrypoint-initdb.d/init-repl.js:ro
+    networks:
+      - box-network
+
+  box-mysql:
+    image: mysql:8
+    restart: unless-stopped
+    profiles:
+      - migration
+    ports:
+      - '3306:3306'
+    environment:
+      MYSQL_ROOT_PASSWORD: rootpass
+      MYSQL_DATABASE: box_admin
+    healthcheck:
+      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+    volumes:
+      - mysql_data:/var/lib/mysql
+    networks:
+      - box-network
+
+  box-mgnt-api:
+    build:
+      context: .
+      dockerfile: apps/box-mgnt-api/Dockerfile
+    ports:
+      - '3300:3300'
+    env_file:
+      - .env.docker
+    environment:
+      NODE_ENV: development
+      APP_HOST: 0.0.0.0
+      APP_PORT: 3300
+      APP_CORS_ORIGIN: '*'
+      REDIS_DB: 0
+      REDIS_TLS: 'false'
+    depends_on:
+      box-redis:
+        condition: service_healthy
+      box-rabbitmq:
+        condition: service_healthy
+      box-mongodb:
+        condition: service_healthy
+    networks:
+      - box-network
+
+  box-app-api:
+    build:
+      context: .
+      dockerfile: apps/box-app-api/Dockerfile
+    ports:
+      - '3301:3301'
+    env_file:
+      - .env.docker
+    environment:
+      NODE_ENV: development
+      APP_HOST: 0.0.0.0
+      APP_PORT: 3301
+      APP_CORS_ORIGIN: '*'
+      REDIS_DB: 1
+      REDIS_TLS: 'false'
+    depends_on:
+      box-redis:
+        condition: service_healthy
+      box-rabbitmq:
+        condition: service_healthy
+      box-mongodb:
+        condition: service_healthy
+    networks:
+      - box-network
+
+  box-stats-api:
+    build:
+      context: .
+      dockerfile: apps/box-stats-api/Dockerfile
+    ports:
+      - '3302:3302'
+    env_file:
+      - .env.docker
+    environment:
+      NODE_ENV: development
+      APP_HOST: 0.0.0.0
+      APP_PORT: 3302
+      APP_CORS_ORIGIN: '*'
+      REDIS_DB: 2
+      REDIS_TLS: 'false'
+    depends_on:
+      box-redis:
+        condition: service_healthy
+      box-rabbitmq:
+        condition: service_healthy
+      box-mongodb:
+        condition: service_healthy
+    networks:
+      - box-network
+
+volumes:
+  redis_data:
+  rabbitmq_data:
+  mongo_data:
+  mysql_data:
+
+networks:
+  box-network:
+    driver: bridge

+ 57 - 0
docker/mongo/init-repl.js

@@ -0,0 +1,57 @@
+(function () {
+  const replSetName = 'rs0';
+  const replSetHost = 'box-mongodb:27017';
+  const config = {
+    _id: replSetName,
+    members: [
+      {
+        _id: 0,
+        host: replSetHost,
+      },
+    ],
+  };
+
+  function replicaSetIsReady() {
+    try {
+      const status = rs.status();
+      return status && status.ok === 1;
+    } catch (error) {
+      print('Replica set status unavailable:', error.message);
+      return false;
+    }
+  }
+
+  if (!replicaSetIsReady()) {
+    print('Initializing replica set', JSON.stringify(config));
+    rs.initiate(config);
+    let attempts = 0;
+    while (attempts < 20) {
+      let status;
+      try {
+        status = rs.status();
+      } catch (error) {
+        print('Replica set not ready yet:', error.message);
+        sleep(1000);
+        attempts += 1;
+        continue;
+      }
+
+      if (status && status.ok === 1 && status.myState === 1) {
+        print('Replica set is PRIMARY');
+        break;
+      }
+
+      print('Waiting for PRIMARY (attempt', attempts + 1, ')');
+      sleep(1000);
+      attempts += 1;
+    }
+  } else {
+    print('Replica set already initialized');
+  }
+
+  ['box_admin', 'box_stats'].forEach((dbName) => {
+    const targetDb = db.getSiblingDB(dbName);
+    const result = targetDb.runCommand({ ping: 1 });
+    print('Ensured database ' + dbName + ':', tojson(result));
+  });
+})();

+ 42 - 0
docs/DOCKER.md

@@ -0,0 +1,42 @@
+# Docker development stack
+
+## Quick start
+
+1. Copy the environment template (only required if you want to override defaults):
+   ```sh
+   cp .env.docker .env
+   ```
+   The stack already reads `.env.docker` via `env_file`, so copying is optional unless you need custom values.
+2. Start the stack:
+   ```sh
+   docker compose up -d
+   ```
+3. When you need to run migrations or any workflow that requires the bundled MySQL, activate the `migration` profile:
+   ```sh
+   docker compose --profile migration up -d box-mysql
+   ```
+   MySQL stays behind this profile so the default stack keeps the scope limited. The APIs already point to `box-mysql` via `MYSQL_URL`, so start the profile before or next to the main stack when migrations are required.
+4. Stop and remove containers/volumes:
+   ```sh
+   docker compose down
+   ```
+
+## Useful helpers
+
+- Stream a service log: `docker compose logs -f box-mgnt-api`
+- Enter the Mongo shell to confirm the replica set: `docker compose exec box-mongodb mongosh --eval 'rs.status()'`
+- Rebuild an API image after code changes: `docker compose build box-mgnt-api`
+
+## Stack summary
+
+- `box-redis`, `box-rabbitmq`, `box-mongodb`, and `box-mysql` use named volumes (`redis_data`, `rabbitmq_data`, `mongo_data`, `mysql_data`). MySQL is only provisioned when you activate the `migration` profile.
+- Mongo runs `mongod --replSet rs0 --bind_ip_all` plus `docker/mongo/init-repl.js`, which initializes `rs0` exactly once and ensures `box_admin`/`box_stats` exist.
+- Each API reads `.env.docker` via `env_file`, then sets host/port/DB overrides for container-specific behavior.
+- One user-defined network (`box-network`) keeps all containers connected while ports stay consistent with the Nest apps (`3300`, `3301`, `3302`).
+
+## Smoke test checklist
+
+- `curl -fsSL http://localhost:3300/api/v1/health` (box-mgnt-api) returns HTTP 200/JSON.
+- `curl -fsSL http://localhost:3301/api/v1/health` (box-app-api) returns HTTP 200/JSON.
+- `curl -fsSL http://localhost:3302/api/v1/health` (box-stats-api) returns HTTP 200/JSON.
+- `docker compose exec box-mongodb mongosh --quiet --eval 'rs.status().ok'` prints `1`.

+ 152 - 0
docs/MGNT_MYSQL_TO_MONGO_MIGRATION.md

@@ -0,0 +1,152 @@
+# Mgnt Storage Migration (MySQL → MongoDB `box_admin`)
+
+## Scope
+- Target: Move **box-mgnt-api** admin storage from MySQL to MongoDB database `box_admin`.
+- Keep **box_stats** unchanged (MongoStats stays as-is).
+- Read-only analysis; no code modifications.
+
+## 1) MySQL Prisma Models Used by box-mgnt-api at Runtime
+
+### Used models (direct Prisma access in mgnt services)
+- `User` (`prisma/mysql/schema/user.prisma`)
+- `Role` (`prisma/mysql/schema/role.prisma`)
+- `UserRole` (`prisma/mysql/schema/user-role.prisma`)
+- `Menu` (`prisma/mysql/schema/menu.prisma`)
+- `RoleMenu` (`prisma/mysql/schema/role-menu.prisma`)
+- `ApiPermission` (`prisma/mysql/schema/api-permission.prisma`)
+- `RoleApiPermission` (`prisma/mysql/schema/role-api-permission.prisma`)
+- `LoginLog` (`prisma/mysql/schema/login-log.prisma`)
+- `OperationLog` (`prisma/mysql/schema/operation-log.prisma`)
+- `QuotaLog` (`prisma/mysql/schema/quota-log.prisma`)
+- `CacheSyncAction` (`prisma/mysql/schema/cache-sync-action.prisma`)
+
+### Not used by box-mgnt-api runtime (present in schema only)
+- `ImageConfig` (no references in `apps/box-mgnt-api/src`)
+- `ProviderVideoSync` (defined in MySQL schema, but mgnt provider sync code writes to Mongo)
+
+## 2) Constraints per Model (PK, Unique, Relations)
+
+### Summary table
+| Model | Primary Key | Unique Constraints | Relations |
+|---|---|---|---|
+| User | `id` (auto-increment int) | `username` | `UserRole.userId -> User.id` |
+| Role | `id` | `name` | `UserRole.roleId -> Role.id`, `RoleMenu.roleId -> Role.id`, `RoleApiPermission.roleId -> Role.id` |
+| UserRole | `id` | `(userId, roleId)` | `userId -> User`, `roleId -> Role` |
+| Menu | `id` | `frontendAuth` | self relation (`parentId -> Menu.id`), `RoleMenu.menuId -> Menu.id`, `ApiPermission.menuId -> Menu.id` |
+| RoleMenu | `id` | `(roleId, menuId)` | `roleId -> Role`, `menuId -> Menu` |
+| ApiPermission | `id` | `(menuId, path, method)` | `menuId -> Menu`, `RoleApiPermission.apiPermissionId -> ApiPermission.id` |
+| RoleApiPermission | `id` | `(roleId, apiPermissionId)` | `roleId -> Role`, `apiPermissionId -> ApiPermission` |
+| LoginLog | `id` | none | none |
+| OperationLog | `id` | none | `menuId` is nullable but not enforced by FK in schema |
+| QuotaLog | `id` | none | none |
+| CacheSyncAction | `id` (BigInt) | none | none; indexed by `status,nextAttemptAt` and `entityType,entityId` |
+
+### Additional field constraints (from schema)
+- Enums used in mgnt:
+  - `MenuType` (`DIRECTORY`, `MENU`, `SUBMENU`, `BUTTON`)
+  - `LoginType` / `LoginStatus`
+  - `OperationType`
+- JSON fields:
+  - `User.twoFARecoveryCodes`, `User.allowIps`
+  - `Menu.meta`
+  - `OperationLog.body`, `OperationLog.response`
+  - `CacheSyncAction.payload`
+- Special uniqueness / lookup usage in code:
+  - `Menu.frontendAuth` queried by `findUnique` in `apps/box-mgnt-api/src/mgnt-backend/core/menu/menu.service.ts`
+
+## 3) Proposed Mongo Collection Schemas + Indexes (`box_admin`)
+
+### Collections (1-to-1 with current MySQL models)
+- `sys_user`
+  - Fields: `id` (int), `username`, `password`, `status`, `nick`, `photo`, `remark`, `twoFA`, `twoFALastUsedStep`, `twoFARecoveryCodes`, `allowIps`, `jwtToken`, `oAuthJwtToken`, `lastLoginTime`, `create_time`, `update_time`
+  - Indexes: unique on `username`, index on `status` (optional for listing)
+- `sys_role`
+  - Fields: `id` (int), `name`, `status`, `remark`, `create_time`, `update_time`
+  - Indexes: unique on `name`
+- `sys_user_role`
+  - Fields: `id` (int), `user_id`, `role_id`, `create_time`, `update_time`
+  - Indexes: unique compound `(user_id, role_id)`; index on `user_id`, `role_id`
+- `sys_menu`
+  - Fields: `id` (int), `parent_id`, `title`, `status`, `type`, `order`, `frontend_auth`, `path`, `name`, `icon`, `redirect`, `component_key`, `meta`, `canView`, `canCreate`, `canUpdate`, `canDelete`, `create_time`, `update_time`
+  - Indexes: unique on `frontend_auth`, index on `parent_id`, index on `type`
+- `sys_role_menu`
+  - Fields: `id` (int), `role_id`, `menu_id`, `canView`, `canCreate`, `canUpdate`, `canDelete`, `create_time`, `update_time`
+  - Indexes: unique compound `(role_id, menu_id)`; index on `role_id`, `menu_id`
+- `sys_api_permission`
+  - Fields: `id` (int), `menu_id`, `path`, `method`, `create_time`, `update_time`
+  - Indexes: unique compound `(menu_id, path, method)`; index on `menu_id`
+- `sys_role_api_permission`
+  - Fields: `id` (int), `role_id`, `api_permission_id`, `create_time`, `update_time`
+  - Indexes: unique compound `(role_id, api_permission_id)`; index on `role_id`, `api_permission_id`
+- `sys_login_log`
+  - Fields: `id` (int), `type`, `status`, `username`, `ip_address`, `user_agent`, `create_time`, `update_time`
+  - Indexes: index on `username`, `create_time`
+- `sys_operation_log`
+  - Fields: `id` (int), `username`, `menu_id`, `description`, `type`, `status`, `method`, `path`, `body`, `response`, `ip_address`, `call_method`, `create_time`, `update_time`
+  - Indexes: index on `username`, `menu_id`, `create_time`
+- `sys_quota_log`
+  - Fields: `id` (int), `username`, `op_username`, `amount`, `is_inc`, `quota`, `remark`, `create_time`, `update_time`
+  - Indexes: index on `username`, `create_time`
+- `cache_sync_action`
+  - Fields: `id` (long), `entityType`, `entityId`, `operation`, `status`, `attempts`, `nextAttemptAt`, `lastError`, `payload`, `createdAt`, `updatedAt`
+  - Indexes: `(status, nextAttemptAt)`, `(entityType, entityId)`
+
+### ID strategy
+- Preserve numeric IDs used by current code (e.g., `User.id`, `Role.id`, `Menu.id`).
+- Use a monotonically increasing counter per collection to emulate auto-increment.
+
+## 4) Relational Assumptions and Transaction Usage
+
+### Joins / relational queries used in mgnt services
+- User ↔ Role via `UserRole` (list, get, update)
+  - `apps/box-mgnt-api/src/mgnt-backend/core/user/user.service.ts`
+  - Uses `include` on `userRoles.role` and `where` clauses with `userRoles.some`
+- Role ↔ Menu via `RoleMenu` (get permissions)
+  - `apps/box-mgnt-api/src/mgnt-backend/core/role/role.service.ts`
+- Menu ↔ ApiPermission via `ApiPermission` + RoleApiPermission
+  - `apps/box-mgnt-api/src/mgnt-backend/core/menu/menu.service.ts`
+- Menu self-relation (parent/children), used for tree building
+  - `apps/box-mgnt-api/src/mgnt-backend/core/menu/menu.service.ts`
+
+### Transaction usage that assumes relational guarantees
+- Role create/update
+  - `RoleService.create` and `RoleService.update` use `$transaction` to insert role + roleMenu or replace roleMenu
+- User create/update/delete
+  - `UserService.create`, `UserService.update`, `UserService.delete` use `$transaction` to change user and userRole together
+- Menu permission update
+  - `MenuService.updatePermission` deletes `roleApiPermission` + `apiPermission`, then recreates in a `$transaction`
+- Menu delete
+  - `MenuService.delete` deletes `roleMenu` + `menu` in a `$transaction`
+- Pagination
+  - `UserService.list` and `RoleService.list` use `$transaction` to fetch list + count
+
+### How to adapt in Mongo
+- Replace relational joins with one of:
+  - **Explicit join collections** (retain `sys_user_role`, `sys_role_menu`, `sys_role_api_permission`) and perform multi-step queries in services.
+  - **Embedded arrays** (e.g., store `roleIds` on `sys_user` and `menuIds` on `sys_role`) to reduce query count.
+- Replace transactions with Mongo multi-document transactions where atomicity is required:
+  - Role create/update (role + role_menu)
+  - User create/update/delete (user + user_role)
+  - Menu permission update (api_permission + role_api_permission)
+- Replace `include` and `some` relational filters with:
+  - Pre-join in application code (fetch roles by roleIds, map)
+  - `$lookup` aggregation pipelines (only if performance is acceptable)
+
+## 5) Migration Design Notes
+- Keep data model parity with existing MySQL shape to minimize code change risk.
+- Preserve unique constraints and compound indexes listed above.
+- Maintain enum values as strings in Mongo to match Prisma usage.
+- Plan for auto-increment simulation using a counters collection (e.g., `counters` with `{ _id: "sys_user", seq: 123 }`).
+
+## Appendix: MySQL Schema Files (Reference)
+- `prisma/mysql/schema/user.prisma`
+- `prisma/mysql/schema/role.prisma`
+- `prisma/mysql/schema/user-role.prisma`
+- `prisma/mysql/schema/menu.prisma`
+- `prisma/mysql/schema/role-menu.prisma`
+- `prisma/mysql/schema/api-permission.prisma`
+- `prisma/mysql/schema/role-api-permission.prisma`
+- `prisma/mysql/schema/login-log.prisma`
+- `prisma/mysql/schema/operation-log.prisma`
+- `prisma/mysql/schema/quota-log.prisma`
+- `prisma/mysql/schema/cache-sync-action.prisma`

+ 204 - 0
docs/MONGODB_REMOVAL_FEASIBILITY.md

@@ -0,0 +1,204 @@
+# MongoDB Removal Feasibility
+
+## Feasibility Summary
+- Removing MongoDB + MongoStats is feasible but high-effort.
+- MongoDB is the primary store for ads, videos, categories, tags, channels, system params, and stats/events.
+- Code paths assume Mongo-native arrays/JSON and ObjectId semantics.
+- A MySQL-only approach requires a full schema replacement plus extensive query rewrites and migration tooling.
+
+## 1) Inventory of Mongo/MongoStats Usage
+
+### Prisma clients
+- Mongo: `@prisma/mongo/client` via `MongoPrismaService` (`libs/db/src/prisma/mongo-prisma.service.ts`)
+- MongoStats: `@prisma/mongo-stats/client` via `MongoStatsPrismaService` (`libs/db/src/prisma/mongo-stats-prisma.service.ts`)
+- App-scoped services:
+  - `apps/box-app-api/src/prisma/prisma-mongo.service.ts`
+  - `apps/box-app-api/src/prisma/prisma-mongo-stats.service.ts`
+  - `apps/box-stats-api/src/prisma/prisma-mongo.service.ts`
+
+### Mongo (main) schema usage
+- Ads / AdsModule (mgnt CRUD, app reads, recommendations, cache warmups)
+  - `apps/box-mgnt-api/src/mgnt-backend/feature/ads/ads.service.ts`
+  - `apps/box-app-api/src/feature/ads/ad.service.ts`
+  - `apps/box-app-api/src/feature/recommendation/recommendation.service.ts`
+  - `libs/core/src/ad/*`, `libs/common/src/services/ad-pool.service.ts`
+- Category / Tag / Channel (mgnt CRUD, app reads, cache builders)
+  - `apps/box-mgnt-api/src/mgnt-backend/feature/category/category.service.ts`
+  - `apps/box-mgnt-api/src/mgnt-backend/feature/tag/tag.service.ts`
+  - `apps/box-mgnt-api/src/mgnt-backend/feature/channel/channel.service.ts`
+  - `apps/box-app-api/src/feature/video/video.service.ts`
+  - `libs/core/src/cache/*`
+- VideoMedia (mgnt CRUD, app reads, recommendation, stats aggregation, cache builders)
+  - `apps/box-mgnt-api/src/mgnt-backend/feature/video-media/video-media.service.ts`
+  - `apps/box-app-api/src/feature/video/video.service.ts`
+  - `apps/box-app-api/src/feature/recommendation/recommendation.service.ts`
+  - `apps/box-stats-api/src/feature/stats-events/stats-aggregation.service.ts`
+  - `libs/core/src/cache/video/*`
+- SystemParam (mgnt CRUD + app reads)
+  - `apps/box-mgnt-api/src/mgnt-backend/feature/system-params/system-params.service.ts`
+  - `apps/box-app-api/src/feature/sys-params/sys-params.service.ts`
+- Sync services (write-heavy)
+  - `apps/box-mgnt-api/src/mgnt-backend/feature/sync-videomedia/sync-videomedia.service.ts`
+  - `apps/box-mgnt-api/src/mgnt-backend/feature/provider-video-sync/provider-video-sync.service.ts`
+- Cache tooling/dev tools
+  - `apps/box-mgnt-api/src/cache-sync/cache-sync.service.ts`
+  - `apps/box-mgnt-api/src/dev/services/video-cache-coverage.service.ts`
+  - `apps/box-mgnt-api/src/dev/services/video-stats.service.ts`
+
+### MongoStats schema usage
+- User + login history
+  - `apps/box-stats-api/src/feature/user-login/user-login.service.ts`
+- Raw events + processed message dedupe
+  - `apps/box-stats-api/src/feature/stats-events/stats-events.consumer.ts`
+- Aggregated stats + Redis sync
+  - `apps/box-stats-api/src/feature/stats-events/stats-aggregation.service.ts`
+
+### Mongo schemas defined but not used
+- `Home` collection (`prisma/mongo/schema/home.prisma`) — no code references found
+- `AdsClickHistory` (`prisma/mongo-stats/schema/ads-click-history.prisma`) — no code references found
+
+## 2) Data Types Stored in Mongo and Usage Patterns
+
+### Core content (Mongo main)
+- Ads / AdsModule
+  - Fields: ObjectId ids, adType enum, start/end BigInt epoch, content/cover/url
+  - Patterns: mgnt CRUD; app reads for lists and recommendation; cache warmups populate Redis
+- Category
+  - Fields: name, subtitle, seq, status, tagNames (String[]), tags (Json)
+  - Patterns: mgnt CRUD; app reads categories; cache builders precompute lists
+- Tag
+  - Fields: name, categoryId (ObjectId), seq/status
+  - Patterns: mgnt CRUD; app reads tags by category; cache builders build tag lists
+- Channel
+  - Fields: channelId (unique), name, URLs, tags/categories as Json/arrays
+  - Patterns: mgnt CRUD; app auth uses channel lookup; cache builders
+- VideoMedia
+  - Large document with arrays (categoryIds, tagIds, tags, actors, secondTags, appids, japanNames), string metadata, BigInt size, DateTime timestamps, tagsFlat
+  - Patterns: mgnt CRUD & sync; app reads by IDs (often from Redis); recommendation queries; stats aggregation needs tagIds; cache builders depend on indices
+
+### System parameters (Mongo main)
+- SystemParam
+  - Fields: id (int), side/type enums, name/value, BigInt timestamps
+  - Patterns: mgnt CRUD; app reads for enums and ad modules
+
+### Stats/event data (MongoStats)
+- User & UserLoginHistory
+  - Writes on login events (upsert user, insert login record)
+  - Reads for stats aggregation/reporting (inferred via indexes)
+- Raw events: AdClickEvents, VideoClickEvents, AdImpressionEvents
+  - Append-only writes from RabbitMQ consumer
+  - Reads by aggregation jobs to compute scores and sync Redis
+- ProcessedMessage
+  - Deduplication on messageId, created by consumer, deleted on failure
+- Aggregated stats: VideoGlobalStats, AdsGlobalStats
+  - Upserted by aggregation jobs; read for scoring/sync to Redis
+- Unused: AdsClickHistory (no code usage)
+
+### Mongo-specific features used
+- ObjectId primary keys stored as strings in code (`@db.ObjectId`)
+- Arrays (String[], Int[]) and Json fields
+- `aggregateRaw` used in `apps/box-app-api/src/feature/video/video.service.ts`
+
+## 3) Minimal MySQL Schema Proposal
+
+### Core content tables
+- `ads`
+  - `id CHAR(24) PRIMARY KEY`, `ads_module_id CHAR(24)`, `advertiser`, `title`, `ads_content`, `ads_cover_img`, `ads_url`, `img_source`, `start_dt BIGINT`, `expiry_dt BIGINT`, `seq INT`, `status INT`, `create_at BIGINT`, `update_at BIGINT`
+  - Index: `ads_module_id`, `status`, `start_dt`, `expiry_dt`
+- `ads_module`
+  - `id CHAR(24) PRIMARY KEY`, `ad_type VARCHAR`, `ads_module VARCHAR`, `module_desc`, `seq INT`
+  - Unique: `ad_type`, `ads_module`
+- `category`
+  - `id CHAR(24) PRIMARY KEY`, `name`, `subtitle`, `seq`, `status`, `create_at BIGINT`, `update_at BIGINT`
+- `tag`
+  - `id CHAR(24) PRIMARY KEY`, `name`, `category_id CHAR(24)`, `seq`, `status`, `create_at BIGINT`, `update_at BIGINT`
+  - Index: `category_id`, `status`
+- `channel`
+  - `id CHAR(24) PRIMARY KEY`, `channel_id VARCHAR UNIQUE`, `name`, `landing_url`, `video_cdn`, `cover_cdn`, `client_name`, `client_notice`, `remark`, `create_at BIGINT`, `update_at BIGINT`
+- `system_param`
+  - `id INT PRIMARY KEY`, `side`, `data_type`, `name`, `value`, `remark`, `create_at BIGINT`, `update_at BIGINT`
+  - Unique: `(side, name)`
+- `video_media`
+  - `id CHAR(24) PRIMARY KEY`, core fields + `tags_flat VARCHAR`, `list_status INT`, `edited_at BIGINT`, `img_source`, provider fields, timestamps
+  - Index: `(list_status, tags_flat)`, `added_time`, `updated_at`
+
+### Join tables to replace arrays
+- `video_media_category` (`video_media_id`, `category_id`, PRIMARY KEY both)
+  - Index on `category_id`
+- `video_media_tag` (`video_media_id`, `tag_id`, PRIMARY KEY both)
+  - Index on `tag_id`
+- Optional denormalized tag names: `video_media_tags` as JSON if minimizing join complexity
+
+### Stats/event tables
+- `user_stats`
+  - `id CHAR(24) PRIMARY KEY`, `uid VARCHAR UNIQUE`, `ip`, `os`, `u_channel_id`, `machine`, `create_at BIGINT`, `last_login_at BIGINT`
+- `user_login_history`
+  - `id CHAR(24) PRIMARY KEY`, `uid`, `ip`, `user_agent`, `app_version`, `os`, `u_channel_id`, `machine`, `create_at BIGINT`, `token_id`
+  - Index: `(uid, create_at)`, `(u_channel_id, create_at)`, `(ip, create_at)`
+- `ad_click_events`, `video_click_events`, `ad_impression_events`
+  - Fields mirror MongoStats schema; index on `(ad_id|video_id, time)`, `(uid, time)`, `(ad_type, time)`
+- `processed_messages`
+  - `message_id VARCHAR UNIQUE`, `event_type`, `processed_at BIGINT`, `created_at BIGINT`
+- `ads_global_stats`, `video_global_stats`
+  - per-id unique rows, indexes on `computed_score`, `computed_recency`, `computed_ctr`, `last_seen_at`
+
+### Notes
+- Use `CHAR(24)` or `BINARY(12)` for former ObjectId values to preserve IDs used in APIs.
+- Arrays used in filters should become join tables; JSON columns can be a stopgap but complicate query parity.
+
+## 4) Migration Plan (Phases, Risks, Rollback)
+
+### Phases
+1. Schema introduction
+   - Add new MySQL tables (content + stats) while keeping Mongo/MongoStats active.
+2. Backfill
+   - One-time migration from Mongo/MongoStats into MySQL (ads, videoMedia, tags, events, stats).
+3. Dual-write
+   - Update write paths to write both Mongo and MySQL (mgnt CRUD, stats consumer, sync services).
+4. Read switch
+   - Shift read paths (app APIs, cache builders, stats aggregation) to MySQL.
+5. Stabilization
+   - Keep Mongo in read-only mode for verification.
+6. Decommission
+   - Remove Mongo dependencies after parity signoff.
+
+### Key risks
+- Array semantics: Mongo arrays for `categoryIds`, `tagIds`, `tagsFlat` used in filters and Redis sync.
+- Aggregation logic: `aggregateRaw` in `apps/box-app-api/src/feature/video/video.service.ts` is Mongo-specific.
+- Event volume: stats events are append-heavy; MySQL indexing/partitioning needed for aggregation performance.
+- ID consistency: APIs and caches assume ObjectId string IDs.
+- Cache rebuilds depend on Mongo data shape and indexing; porting to MySQL must preserve ordering and filters.
+
+### Rollback strategy
+- Keep Mongo/MongoStats live during migration.
+- If errors: switch reads back to Mongo, stop MySQL writes, keep MySQL for postmortem.
+- Preserve dual-write toggles to flip back quickly.
+
+## 5) Test Checklist for Parity
+
+### API parity
+- Ads API: `/api/v1/ads/list`, `/api/v1/ads/click`, `/api/v1/ads/impression`
+- Video API: `/api/v1/video/list`, `/api/v1/video/search-by-tag`, `/api/v1/video/category/:id/latest`, `/api/v1/video/recommended`
+- Homepage API: `/api/v1/homepage` (ads, categories, announcements, videos)
+- Recommendation APIs: `/api/v1/recommend/*`, `/api/v1/recommendation/*`
+
+### Mgnt parity
+- CRUD: ads, categories, tags, channels, system params, video media
+- Sync endpoints: provider sync and video sync flow
+- Cache rebuild endpoints: cache-sync and admin rebuild
+
+### Stats parity
+- RabbitMQ ingestion → event writes
+- Aggregation results: `ads_global_stats`, `video_global_stats` values and Redis scores
+- Redis sync results: `ads:global:score`, `video:global:score`, `video:tag:*:score`
+
+### Performance
+- Cache rebuild job duration and Redis write throughput
+- Video listing latency (paged list by category/tag)
+- Recommendation query latency (similar ads/videos)
+- Stats aggregation runtime with realistic event volumes
+
+### Data integrity
+- ID mapping unchanged (ObjectId format preserved)
+- Tags/categories relations consistent for video media
+- No loss of BigInt epoch precision in timestamps

+ 223 - 0
docs/PROJECT_STATE_SUMMARY.md

@@ -0,0 +1,223 @@
+# Project State Summary
+
+## Implementation Summary
+
+### 1. Project structure overview (apps / libs / packages actually present)
+- apps: `apps/box-app-api`, `apps/box-mgnt-api`, `apps/box-stats-api`
+- libs: `libs/common`, `libs/core`, `libs/db`
+- packages: none present
+- data/infra: `prisma/mysql`, `prisma/mongo`, `prisma/mongo-stats`
+- docs/artifacts: `docs`, `dist`, `logs`, `build.log`, `structures.txt`
+
+### 2. Implemented backend applications (what can actually boot/run)
+- box-mgnt-api: Fastify-based NestJS app (`apps/box-mgnt-api/src/main.ts`)
+- box-app-api: Express-based NestJS app (`apps/box-app-api/src/main.ts`)
+- box-stats-api: Express-based NestJS app (`apps/box-stats-api/src/main.ts`)
+
+### 3. Implemented modules per app (auth, user, config, etc.)
+- box-mgnt-api
+  - Core + auth: Auth, User, Role, Menu, LoginLog, OperationLog, QuotaLog
+  - Feature: Ads, Category, Channel, Tag, VideoMedia, SystemParams, SyncVideomedia, ProviderVideoSync, S3, Health, ImageUpload, MgntHttpService
+  - Cache: CacheSyncModule, CoreModule (cache warmups/builders), RedisModule
+  - Cross-cutting: CommonModule (global interceptors/filters/logging), SharedModule (Prisma, HttpModule)
+  - Dev-only: DevVideoCacheModule (loaded only when `NODE_ENV !== 'production'`)
+  - UNUSED: OssModule exists in `apps/box-mgnt-api/src/mgnt-backend/feature/oss/oss.module.ts` but is commented out in `apps/box-mgnt-api/src/mgnt-backend/feature/feature.module.ts`
+- box-app-api
+  - Auth, Ads, Video, Recommendation, Homepage, SysParams, Stats, Health
+  - Infrastructure: PrismaMongoModule (Mongo + MongoStats Prisma services), RedisModule, RedisCacheModule (cache-manager), RabbitmqModule
+- box-stats-api
+  - UserLoginModule, RabbitmqConsumerModule, StatsEventsModule, PrismaMongoModule, RedisModule
+
+### 4. Implemented infrastructure
+- database(s): MySQL (`prisma/mysql`), MongoDB (`prisma/mongo`), MongoDB stats (`prisma/mongo-stats`)
+- ORM/ODM: Prisma (`@prisma/client`), services in `libs/db/src/prisma/*`
+- cache: Redis via `ioredis` (`libs/db/src/redis/*`), cache-manager via `cache-manager-ioredis-yet` in `apps/box-app-api/src/redis/redis-cache.module.ts`
+- message queue: RabbitMQ via `amqplib` (`apps/box-app-api/src/rabbitmq/*`, `apps/box-stats-api/src/feature/rabbitmq/*`)
+
+### 5. Implemented cross-cutting concerns
+- auth / guards
+  - box-mgnt-api: global JWT + RBAC guards (`apps/box-mgnt-api/src/mgnt-backend/core/auth/auth.module.ts`), LocalAuthGuard for login, RateLimitGuard on auth endpoints, MFA flow in auth service/guards
+  - box-app-api: JwtAuthGuard used on specific endpoints (ads click/impression, stats events, video click, rabbitmq fallback)
+  - UNUSED: `MfaGuard` exists only in `libs/common/src/guards/mfa.guard.ts` with no references
+- exception filters
+  - HttpExceptionFilter applied globally via `libs/common/src/common.module.ts` and `apps/box-app-api/src/app.module.ts`
+  - UNUSED: `AllExceptionsFilter` exists only in `libs/common/src/filters/all-exceptions.filter.ts`
+- logging
+  - box-mgnt-api: `nestjs-pino` + Correlation/Logging/OperationLog/Response interceptors in `libs/common/src/common.module.ts`
+  - box-app-api / box-stats-api: Nest Logger in `main.ts`
+- config loading
+  - box-mgnt-api: `ConfigModule` with env validation (`apps/box-mgnt-api/src/config/env.validation.ts`)
+  - box-app-api: `ConfigModule` with `.env.app` + `.env`
+  - box-stats-api: `ConfigModule` with `.env.stats` + `.env`
+
+### 6. Implemented APIs/endpoints (high-level, no payload details)
+- box-app-api (global prefix `/api/v1`)
+  - Auth: `/auth/login`
+  - Ads: `/ads/list`, `/ads/click`, `/ads/impression`
+  - Video: `/video/recommended`, `/video/category/:channelId/:categoryId/latest`, `/video/categories-with-tags`, `/video/list`, `/video/search-by-tag`, `/video/click`
+  - Recommendation (public): `/recommend/video/:videoId`, `/recommend/ad/:adId`
+  - Recommendation (internal-style): `/api/v1/recommendation/videos/:videoId/similar`, `/api/v1/recommendation/ads/:adId/similar`, `/api/v1/recommendation/ads/:adId/similar-simple`
+  - Homepage: `/homepage`
+  - Sys params: `/sys-params/ad-types`
+  - Stats events: `/stats/ad/click`, `/stats/video/click`, `/stats/ad/impression`
+  - Health: `/health`
+  - RabbitMQ: `/rabbitmq/fallback/replay`, `/rabbitmq/fallback/status`, `/rabbitmq/fallback/queue-size`, `/api/v1/internal/rabbitmq/status`
+- box-stats-api (global prefix `/api/v1`)
+  - Internal stats: `/internal/stats/ingestion`, `/internal/stats/aggregate/ads`, `/internal/stats/aggregate/videos`
+  - Debug: `/internal/stats/debug/redis/videos/top`, `/internal/stats/debug/redis/ads/top`
+- box-mgnt-api (global prefix `/api/v1`, routed under `/mgnt`)
+  - Auth: `/api/v1/mgnt/auth/login`, `/login2fa`, `/logout`, `/2fa/generate`, `/2fa/setup`, `/2fa/enable`, `/permission`, `/abilities`
+  - Users: `/api/v1/mgnt/users` (CRUD + list/search), `/me`, `/me/password`, `/me/personal-info`, `/role/:roleId`, `/admin-remove-2fa`, `/admin-set-2fa`, `/reset-password`
+  - Roles: `/api/v1/mgnt/roles` (list/create/update/delete/permissions)
+  - Menus: `/api/v1/mgnt/menus` (list/tree/create/update/delete)
+  - Logs: `/api/v1/mgnt/login-logs`, `/operation-logs`, `/quota-logs`
+  - System params: `/api/v1/mgnt/system-param` (list/create/update/get)
+  - Ads: `/api/v1/mgnt/ads` (list/get/create/update/delete), `/ads/:id/cover`, `/ads/modules/list`
+  - Ads legacy images: `/api/v1/mgnt/ads-legacy/*`
+  - Categories: `/api/v1/mgnt/categories`
+  - Channels: `/api/v1/mgnt/channels`
+  - Tags: `/api/v1/mgnt/tags`
+  - Video media: `/api/v1/mgnt/video-media` (list/create/update/status/sync/etc.)
+  - Sync: `/api/v1/mgnt/sync-videomedia`, `/api/v1/mgnt/provider-video-sync`
+  - S3 utilities: `/api/v1/mgnt/s3/*` (policy, signed URL/image/video helpers)
+  - Health: `/api/v1/mgnt/health/redis`
+  - Cache sync/admin: `/api/v1/mgnt/cache/*`, `/mgnt-debug/cache-sync/*`, `/api/v1/mgnt/admin/cache/video/*`
+  - Dev-only (non-prod): `/api/v1/mgnt/dev/cache/*`, `/api/v1/mgnt/dev/video/*`
+
+### 7. Implemented build / dev / deploy scripts
+- Dev: `dev:mgnt`, `dev:app`, `dev:stats`
+- Build: `build:mgnt`, `build:app`, `build:stats`
+- Start: `start:mgnt`, `start:app`, `start:stats`
+- Prisma: generate/migrate/seed/setup for MySQL and Mongo
+- Tooling: `typecheck`, `typecheck:watch`, `lint`, `lint:fix`
+
+### 8. Known constraints enforced in code
+- Global API prefixing: `/api/v1` on all apps; management APIs additionally under `/mgnt` router
+- ValidationPipe with `whitelist` and `transform` enabled (all apps)
+- CORS configured by env (`APP_CORS_ORIGIN`) with defaults and method restrictions
+- File upload size limit 30MB for mgnt Fastify multipart
+- Internal endpoints guarded by environment checks (RabbitMQ status) and dev-only module gating by `NODE_ENV`
+- Auth enforcement: mgnt uses global JWT + RBAC guards; login endpoints rate-limited; app/stats use JwtAuthGuard on selected endpoints
+- BigInt JSON serialization override in mgnt main
+
+### 9. Explicit TODOs or TODO comments found in code
+- `apps/box-stats-api/src/feature/stats-events/stats-aggregation.service.ts`: TODO for tag-based ad stats
+- `apps/box-mgnt-api/src/mgnt-backend/feature/video-media/video-media.controller.ts`: TODO for delete video media
+- `apps/box-mgnt-api/src/cache-sync/cache-sync.service.ts`: TODO for HOME/CHANNEL/TRENDING list rebuild
+- `libs/core/src/cache/video/list/video-list-cache.builder.ts`: multiple TODOs for refactor after schema change
+
+### 10. Ambiguous or risky areas that require human confirmation
+- UNCLEAR – controller paths in `apps/box-app-api/src/feature/recommendation/recommendation.controller.ts` and `apps/box-app-api/src/rabbitmq/rabbitmq-status.controller.ts` hardcode `api/v1` while `apps/box-app-api/src/main.ts` also sets a global `api/v1` prefix; confirm whether routes are intended to be `/api/v1/api/v1/...` or just `/api/v1/...`
+- UNCLEAR – `GET /api/v1/video/recommended` reads `req.user` but has no guard; confirm if another auth middleware populates `req.user` or if this is intended to be public
+- UNCLEAR – `apps/box-stats-api/src/feature/stats-events/stats-internal.controller.ts` exposes internal/debug endpoints without auth; confirm expected protection model
+- UNUSED – commented endpoints (e.g., `apps/box-app-api/src/feature/sys-params/sys-params.controller.ts` constants, `apps/box-app-api/src/feature/ads/ad.controller.ts` ad URL, `apps/box-mgnt-api/src/mgnt-backend/core/auth/auth.controller.ts` OAuth routes) appear intentionally disabled; confirm whether they should stay disabled
+
+## Feature Classification
+
+| Feature / Module | Status | Evidence in Code | Risk if Touched |
+|---|---|---|---|
+| box-mgnt-api app | PRODUCTION-READY | `apps/box-mgnt-api/src/main.ts`, `apps/box-mgnt-api/src/app.module.ts`, scripts `dev:mgnt/start:mgnt` in `package.json` | High: core admin backend |
+| Mgnt Core Auth (JWT/RBAC/2FA) | PRODUCTION-READY | `apps/box-mgnt-api/src/mgnt-backend/core/auth/auth.module.ts` is imported via `apps/box-mgnt-api/src/mgnt-backend/mgnt-backend.module.ts` | High: login + permissions |
+| Mgnt Users | PRODUCTION-READY | `apps/box-mgnt-api/src/mgnt-backend/core/user/user.module.ts` wired in `apps/box-mgnt-api/src/mgnt-backend/mgnt-backend.module.ts` | High: admin user mgmt |
+| Mgnt Roles | PRODUCTION-READY | `apps/box-mgnt-api/src/mgnt-backend/core/role/role.module.ts` wired in `apps/box-mgnt-api/src/mgnt-backend/mgnt-backend.module.ts` | High: RBAC |
+| Mgnt Menus | PRODUCTION-READY | `apps/box-mgnt-api/src/mgnt-backend/core/menu/menu.module.ts` wired in `apps/box-mgnt-api/src/mgnt-backend/mgnt-backend.module.ts` | Medium: admin UI navigation |
+| Mgnt Logs (login/operation/quota) | PRODUCTION-READY | `apps/box-mgnt-api/src/mgnt-backend/core/logging/*` wired in `apps/box-mgnt-api/src/mgnt-backend/mgnt-backend.module.ts` | Medium: audit trails |
+| Mgnt Ads management | PRODUCTION-READY | `apps/box-mgnt-api/src/mgnt-backend/feature/ads/ads.module.ts` wired in `apps/box-mgnt-api/src/mgnt-backend/mgnt-backend.module.ts` | High: ad CRUD |
+| Mgnt Categories | PRODUCTION-READY | `apps/box-mgnt-api/src/mgnt-backend/feature/category/category.module.ts` wired in `apps/box-mgnt-api/src/mgnt-backend/mgnt-backend.module.ts` | Medium: app taxonomy |
+| Mgnt Channels | PRODUCTION-READY | `apps/box-mgnt-api/src/mgnt-backend/feature/channel/channel.module.ts` wired in `apps/box-mgnt-api/src/mgnt-backend/mgnt-backend.module.ts` | Medium |
+| Mgnt Tags | PRODUCTION-READY | `apps/box-mgnt-api/src/mgnt-backend/feature/tag/tag.module.ts` wired in `apps/box-mgnt-api/src/mgnt-backend/mgnt-backend.module.ts` | Medium |
+| Mgnt Video Media | PRODUCTION-READY | `apps/box-mgnt-api/src/mgnt-backend/feature/video-media/video-media.module.ts` wired in `apps/box-mgnt-api/src/mgnt-backend/mgnt-backend.module.ts` | High: video catalog |
+| Mgnt System Params | PRODUCTION-READY | `apps/box-mgnt-api/src/mgnt-backend/feature/system-params/system-params.module.ts` wired in `apps/box-mgnt-api/src/mgnt-backend/mgnt-backend.module.ts` | Medium |
+| Mgnt Sync Video Media | PRODUCTION-READY | `apps/box-mgnt-api/src/mgnt-backend/feature/sync-videomedia/sync-videomedia.module.ts` wired in `apps/box-mgnt-api/src/mgnt-backend/mgnt-backend.module.ts` | Medium: sync logic |
+| Mgnt Provider Video Sync | PRODUCTION-READY | `apps/box-mgnt-api/src/mgnt-backend/feature/provider-video-sync/provider-video-sync.module.ts` wired in `apps/box-mgnt-api/src/mgnt-backend/feature/feature.module.ts` | Medium |
+| Mgnt S3 endpoints | PRODUCTION-READY | `apps/box-mgnt-api/src/mgnt-backend/feature/s3/s3.module.ts` wired in `apps/box-mgnt-api/src/mgnt-backend/mgnt-backend.module.ts` | Medium: upload/signing |
+| Mgnt Health (Redis) | INTERNAL-ONLY | `apps/box-mgnt-api/src/mgnt-backend/feature/health/health.module.ts` wired in `apps/box-mgnt-api/src/mgnt-backend/mgnt-backend.module.ts` | Low: monitoring |
+| Mgnt Cache Sync (admin/debug) | INTERNAL-ONLY | `apps/box-mgnt-api/src/cache-sync/cache-sync.module.ts` wired in `apps/box-mgnt-api/src/app.module.ts` | Medium: cache integrity |
+| Mgnt Dev Video Cache tools | INTERNAL-ONLY | `apps/box-mgnt-api/src/dev/dev-video-cache.module.ts` only imported when `NODE_ENV !== 'production'` in `apps/box-mgnt-api/src/app.module.ts` | Medium: dev diagnostics |
+| Mgnt ImageUpload service | PRODUCTION-READY | `apps/box-mgnt-api/src/mgnt-backend/feature/image-upload/image-upload.module.ts` imported by feature services (ads/video-media) | Medium: media pipeline |
+| Mgnt MgntHttpService | PRODUCTION-READY | `apps/box-mgnt-api/src/mgnt-backend/feature/mgnt-http-service/mgnt-http-service.module.ts` wired in `apps/box-mgnt-api/src/mgnt-backend/feature/feature.module.ts` | Low |
+| Mgnt OSS module | DEAD / UNUSED | `apps/box-mgnt-api/src/mgnt-backend/feature/oss/oss.module.ts` not imported (commented in `apps/box-mgnt-api/src/mgnt-backend/feature/feature.module.ts`) | Low: unused |
+| box-app-api app | PRODUCTION-READY | `apps/box-app-api/src/main.ts`, `apps/box-app-api/src/app.module.ts`, scripts `dev:app/start:app` in `package.json` | High: public API |
+| App Auth (JWT) | PRODUCTION-READY | `apps/box-app-api/src/feature/auth/auth.module.ts` imported in `apps/box-app-api/src/app.module.ts` | High |
+| App Ads API | PRODUCTION-READY | `apps/box-app-api/src/feature/ads/ad.module.ts` imported in `apps/box-app-api/src/app.module.ts` | High |
+| App Video API | PRODUCTION-READY | `apps/box-app-api/src/feature/video/video.module.ts` imported in `apps/box-app-api/src/app.module.ts` | High |
+| App Homepage API | PRODUCTION-READY | `apps/box-app-api/src/feature/homepage/homepage.module.ts` imported in `apps/box-app-api/src/app.module.ts` | Medium |
+| App Sys Params API | PRODUCTION-READY | `apps/box-app-api/src/feature/sys-params/sys-params.module.ts` imported in `apps/box-app-api/src/app.module.ts` | Medium |
+| App Recommendation API (public) | PRODUCTION-READY | `apps/box-app-api/src/feature/recommendation/recommendation.module.ts` imported in `apps/box-app-api/src/app.module.ts` | Medium |
+| App Stats Events publisher | PRODUCTION-READY | `apps/box-app-api/src/feature/stats/stats.module.ts` imported in `apps/box-app-api/src/app.module.ts` | Medium |
+| App Health endpoint | INTERNAL-ONLY | `apps/box-app-api/src/health/health.module.ts` imported in `apps/box-app-api/src/app.module.ts` | Low |
+| App RabbitMQ publisher + fallback | INTERNAL-ONLY | `apps/box-app-api/src/rabbitmq/rabbitmq.module.ts` imported in `apps/box-app-api/src/app.module.ts` | Medium: event pipeline |
+| App RabbitMQ status endpoint | INTERNAL-ONLY | env guard in `apps/box-app-api/src/rabbitmq/rabbitmq-status.controller.ts` | Low |
+| box-stats-api app | PRODUCTION-READY | `apps/box-stats-api/src/main.ts`, `apps/box-stats-api/src/app.module.ts`, scripts `dev:stats/start:stats` in `package.json` | High: stats processing |
+| Stats RabbitMQ consumer | PRODUCTION-READY | `apps/box-stats-api/src/feature/rabbitmq/rabbitmq-consumer.module.ts` imported in `apps/box-stats-api/src/app.module.ts` | Medium |
+| Stats aggregation scheduler | PRODUCTION-READY | `apps/box-stats-api/src/feature/stats-events/stats-events.module.ts` imported in `apps/box-stats-api/src/app.module.ts` | Medium |
+| Stats internal/debug endpoints | INTERNAL-ONLY | `apps/box-stats-api/src/feature/stats-events/stats-internal.controller.ts` | Low |
+| Shared Prisma services (MySQL/Mongo/MongoStats) | PRODUCTION-READY | `libs/db/src/prisma/prisma.module.ts` exported by `libs/db/src/shared.module.ts` used in mgnt/stats | High: data access |
+| Redis module/service | PRODUCTION-READY | `libs/db/src/redis/redis.module.ts` used in all apps | High: cache/messaging |
+| CommonModule interceptors/filters | PRODUCTION-READY | `libs/common/src/common.module.ts` imported in `apps/box-mgnt-api/src/app.module.ts` | Medium: response/logging |
+| AllExceptionsFilter | DEAD / UNUSED | `libs/common/src/filters/all-exceptions.filter.ts` no wiring found | Low |
+| MfaGuard | DEAD / UNUSED | `libs/common/src/guards/mfa.guard.ts` no usage | Low |
+
+## How This Project Currently Runs
+
+### 1) Start `box-mgnt-api`
+- Command(s): `pnpm dev:mgnt` (nest start), or `pnpm build:mgnt` then `pnpm start:mgnt` (`node dist/apps/box-mgnt-api/src/main.js`)
+- Env loading: `ConfigModule.forRoot` uses `.env.mgnt` then `.env` in `apps/box-mgnt-api/src/app.module.ts`
+- REQUIRED (validated): `MYSQL_URL`, `MONGO_URL`, `JWT_SECRET`, `REDIS_HOST`, `REDIS_PORT`
+- REQUIRED in practice: `MONGO_STATS_URL` (Prisma MongoStats connects on init; schema expects `env("MONGO_STATS_URL")`)
+- OPTIONAL (defaults/hardcoded): `APP_HOST`, `HOST`, `APP_PORT`/`PORT`, `APP_CORS_ORIGIN`, `REDIS_PASSWORD`, `REDIS_DB`, `REDIS_KEY_PREFIX`, `REDIS_TLS`, `JWT_EXPIRES_IN_SECONDS`, `ENCRYPTION_KEY`
+- External services contacted at runtime: MySQL (Prisma connect), MongoDB (Prisma connect), MongoStats (Prisma connect), Redis (ioredis client init). S3 only when S3 endpoints are used; `S3Service` relies on `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_S3_REGION_NAME`, `AWS_STORAGE_BUCKET_NAME`, optional `AWS_S3_ENDPOINT_URL`
+
+### 2) Start `box-app-api`
+- Command(s): `pnpm dev:app` (uses `dotenv -e .env.app -- nest start ...`), or `pnpm build:app` then `pnpm start:app` (`node dist/apps/box-app-api/src/main.js`)
+- Env loading: `ConfigModule.forRoot` uses `.env.app` then `.env` in `apps/box-app-api/src/app.module.ts` (dev command also sets env)
+- REQUIRED: `MONGO_URL`, `MONGO_STATS_URL` (Prisma Mongo + MongoStats clients connect on init; schemas use `env("MONGO_URL")` and `env("MONGO_STATS_URL")`)
+- REQUIRED in practice: Redis connection parameters; defaults are used if unset (`REDIS_HOST` defaults to `127.0.0.1`, `REDIS_PORT` defaults to `6379`)
+- OPTIONAL (defaults/hardcoded): `APP_HOST`, `HOST`, `APP_PORT`/`PORT`, `APP_CORS_ORIGIN`, `IMAGE_ROOT_PATH` (default `/data/box-images`), `JWT_SECRET` (defaults to `default-secret-key`), `JWT_EXPIRES_IN` (defaults to `7d`)
+- RabbitMQ: `RABBITMQ_URL` is OPTIONAL; if missing, publisher logs and stays open-circuit
+- External services contacted at runtime: MongoDB, MongoStats, Redis (both RedisModule and cache-manager). RabbitMQ if `RABBITMQ_URL` is set. Filesystem for static images at `IMAGE_ROOT_PATH` via Express static middleware
+
+### 3) Start `box-stats-api`
+- Command(s): `pnpm dev:stats` (uses `dotenv -e .env.stats -- nest start ...`), or `pnpm build:stats` then `pnpm start:stats` (`node dist/apps/box-stats-api/src/main.js`)
+- Env loading: `ConfigModule.forRoot` uses `.env.stats` then `.env` in `apps/box-stats-api/src/app.module.ts`
+- REQUIRED: `MONGO_STATS_URL` (Prisma MongoStats connects on init)
+- REQUIRED in practice: Redis connection parameters; defaults are used if unset (`REDIS_HOST` default `127.0.0.1`, `REDIS_PORT` default `6379`)
+- OPTIONAL (defaults/hardcoded): `APP_HOST`, `HOST`, `APP_PORT`/`PORT`, `APP_CORS_ORIGIN`
+- RabbitMQ: `RABBITMQ_URL` OPTIONAL; consumer logs error and returns without connecting if missing
+- External services contacted at runtime: MongoStats, Redis. RabbitMQ only if `RABBITMQ_URL` is set
+
+### Hardcoded/default behaviors to note
+- `box-app-api` JWT secret defaults to `default-secret-key` and expiration defaults to `7d` if env is missing
+- All apps default to binding host `0.0.0.0` and ports `3300/3301/3302` if `APP_PORT`/`PORT` not set
+- `box-app-api` and `box-mgnt-api` serve static images from `/data/box-images` if `IMAGE_ROOT_PATH` is not set
+
+## Plan vs Current Implementation
+
+### 1. Features already implemented beyond the original plan
+- box-stats-api exists and runs with RabbitMQ consumer + aggregation + internal endpoints, while docs call it “future/upcoming” (`docs/ARCHITECTURE.md`, `docs/SYSTEM_OVERVIEW.md`, `apps/box-stats-api/src/app.module.ts`, `apps/box-stats-api/src/feature/stats-events/*`)
+- Recommendation APIs (similar ads/videos + “guess you like”) are implemented but not mentioned in architecture/roadmap (`apps/box-app-api/src/feature/recommendation/*`)
+- RabbitMQ resilience layer with fallback replay + DLQ for stats events exists but is not called out in early planning docs (`apps/box-app-api/src/rabbitmq/rabbitmq-publisher.service.ts`, `apps/box-app-api/src/rabbitmq/rabbitmq-fallback-replay.controller.ts`)
+- Dev-only cache tooling and production cache-admin endpoints exist beyond high-level planning notes (`apps/box-mgnt-api/src/dev/*`, `apps/box-mgnt-api/src/cache-sync/*`)
+
+### 2. Planned items that are not implemented
+- TODO: delete video media endpoint (`apps/box-mgnt-api/src/mgnt-backend/feature/video-media/video-media.controller.ts`)
+- TODO: cache rebuild for HOME/CHANNEL/TRENDING lists (`apps/box-mgnt-api/src/cache-sync/cache-sync.service.ts`)
+- TODO: tag-based ad aggregation (`apps/box-stats-api/src/feature/stats-events/stats-aggregation.service.ts`)
+- TODO: refactors after schema changes for video list cache (`libs/core/src/cache/video/list/video-list-cache.builder.ts`)
+- Planned “optimization/scalability/monitoring” phases are not represented as concrete modules in code (Phase 5–8 in `docs/ROADMAP.md`)
+
+### 3. Design decisions that diverged from early planning
+- “Stats API (future/upcoming)” in planning docs diverges from code that already implements and wires it (`docs/ARCHITECTURE.md`, `docs/SYSTEM_OVERVIEW.md`, `apps/box-stats-api/src/*`)
+- “Redis-first with Mongo fallback (future extension)” diverges from app code that directly uses Mongo Prisma in multiple services (`apps/box-app-api/src/feature/*/*.service.ts`)
+
+### 4. Irreversible decisions (schema, contracts, public APIs)
+- Database schemas are established in Prisma for MySQL/Mongo/MongoStats (`prisma/mysql/schema/*.prisma`, `prisma/mongo/schema/*.prisma`, `prisma/mongo-stats/schema/*.prisma`)
+- Public API route structure is fixed to `/api/v1` and `/api/v1/mgnt/*` with specific controllers already defined (`apps/*/src/main.ts`, `apps/box-mgnt-api/src/mgnt-backend/mgnt-backend.module.ts`, controller files under `apps/box-app-api/src/feature/*` and `apps/box-mgnt-api/src/mgnt-backend/*`)
+- Stats ingestion routes and internal debug endpoints are defined and exposed in code, creating implied contracts (`apps/box-app-api/src/feature/stats/stats.controller.ts`, `apps/box-stats-api/src/feature/stats-events/stats-internal.controller.ts`)
+
+## Resume Checklist
+
+- Safe to continue: `apps/box-app-api/src/feature/homepage`, `apps/box-app-api/src/feature/sys-params`, `apps/box-mgnt-api/src/mgnt-backend/feature/tag`, `apps/box-mgnt-api/src/mgnt-backend/feature/category`, `apps/box-mgnt-api/src/mgnt-backend/feature/channel`, `apps/box-mgnt-api/src/mgnt-backend/feature/system-params` (isolated CRUD, clear module wiring)
+- Fragile—don’t touch without tests: auth/guards (`apps/box-mgnt-api/src/mgnt-backend/core/auth`, `apps/box-app-api/src/feature/auth`), cache sync/warmups (`apps/box-mgnt-api/src/cache-sync`, `libs/core/src/cache/*`), RabbitMQ publisher/consumer (`apps/box-app-api/src/rabbitmq`, `apps/box-stats-api/src/feature/rabbitmq`)
+- Missing glue code blocking features: TODOs in cache rebuild paths (`apps/box-mgnt-api/src/cache-sync/cache-sync.service.ts`), video list cache refactors (`libs/core/src/cache/video/list/video-list-cache.builder.ts`), video media delete endpoint (`apps/box-mgnt-api/src/mgnt-backend/feature/video-media/video-media.controller.ts`)
+- Unknowns to clarify before new work: route prefixing ambiguity (`apps/box-app-api/src/feature/recommendation/recommendation.controller.ts` vs global prefix), auth expectations for `GET /api/v1/video/recommended`, stats internal endpoints access model, required env vars for S3 usage (`apps/box-mgnt-api/src/mgnt-backend/feature/s3/s3.service.ts`) and RabbitMQ availability expectations