Skip to content

feat(ci): add GitHub Actions CI/CD workflow and Dockerfiles - #2

Merged
DavidHLP merged 8 commits into
mainfrom
feat/ci-cd-workflow
Feb 26, 2026
Merged

feat(ci): add GitHub Actions CI/CD workflow and Dockerfiles#2
DavidHLP merged 8 commits into
mainfrom
feat/ci-cd-workflow

Conversation

@DavidHLP

Copy link
Copy Markdown
Owner

Summary

  • Add comprehensive CI workflow with lint, type-check, test, build, and docker jobs
  • Add multi-stage Dockerfiles for all three packages (backend, console, management)
  • Add nginx configs for SPA routing with security headers
  • Add .dockerignore for optimized Docker builds
  • Remove obsolete .husky/pre-commit hook

Changes

CI Workflow (.github/workflows/ci.yml)

  • setup: Install and cache dependencies with pnpm
  • lint: Parallel lint for all 3 packages
  • type-check: Parallel type checking (with Prisma generate for backend)
  • test: Parallel tests with MySQL 9.1 + Redis 7 services for backend
  • build: Parallel build for all packages
  • docker: Build & push to ghcr.io (main branch only)

Dockerfiles

  • backend: Multi-stage NestJS production image with Prisma
  • console: Vue 3 + Vite build + nginx for SPA
  • management: Vue 3 + Vite build + nginx for SPA

Other Files

  • console/nginx.conf / management/nginx.conf: SPA routing with security headers
  • .dockerignore: Exclude unnecessary files from Docker builds

Docker Images (pushed on main)

  • ghcr.io/<owner>/ulticode-backend:<sha> and latest
  • ghcr.io/<owner>/ulticode-console:<sha> and latest
  • ghcr.io/<owner>/ulticode-management:<sha> and latest

Test Plan

  • Verify CI runs successfully on this PR
  • Confirm all lint, type-check, and test jobs pass
  • Verify build jobs complete for all packages
  • After merge, confirm Docker images are pushed to ghcr.io

- Add CI workflow with lint, type-check, test, build, and docker jobs
- Add multi-stage Dockerfiles for backend (NestJS), console, and management (Vue + nginx)
- Add nginx configs for SPA routing with security headers
- Add .dockerignore for optimized Docker builds
- Remove obsolete .husky/pre-commit hook

CI runs on push/PR to main with MySQL and Redis services for backend tests.
Docker images pushed to ghcr.io on main branch merges.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

## Phase 1: Critical Security Fixes
- Docker sandbox for secure code execution with resource limits
- Test case management system with hidden test cases
- Rate limiting with tiered limits per endpoint type

## Phase 2: Core Features
- Global search with MeiliSearch integration
- Real-time notifications via Socket.IO with Redis adapter
- Achievement system with user progress tracking
- Multi-language support (JS, TS, Python, Java, C++, Go)

## Phase 3.1: Code Editor Enhancement
- Theme switching with localStorage persistence
- Font size adjustment (10-24px)
- Code templates for 7 languages (16 templates)
- Keyboard shortcuts reference modal

## Phase 3.2: Loading States & Error Handling
- Global loading overlay with spinner
- Error boundary component for graceful error handling
- Retry button with exponential backoff
- Offline indicator with network status detection
- Global error handler plugin with toast notifications
- Add Skill Radar Chart to PersonalView showing problem-solving strengths by tag
- Add GET /users/:id/skills endpoint to aggregate solved problems by tags
- Implement virtual scrolling in DataTable component using @tanstack/vue-virtual
- Add database indexes for Problem (difficulty, is_published, is_deleted) and Submission (problem_id, created_at)
- Add @tanstack/vue-virtual package for efficient rendering of large lists
- Add i18n translations for skills section in English and Chinese
Backend features:
- Add backup/recovery system with scheduling and restore capabilities
- Add email management service with template support
- Add system monitoring with real-time metrics
- Implement cache interceptor for response optimization
- Enhance code sandbox with Docker and VM support
- Add rate limiting with custom throttle guard
- Improve test case management with hidden test support

Console features:
- Add loading overlay, error boundary, and skeleton components
- Enhance virtual scrolling in DataTable component
- Improve error handling and user feedback

Management features:
- Add system views (Backup, Email, Monitoring)
- Add hidden test cases editor component
- Improve router configuration with lazy loading

Cleanup:
- Remove eslintcache files from version control
…nd new features

- Fix backend throttle guard dependency injection using getOptionsToken/getStorageToken
- Add session flag to prevent unnecessary /auth/me API calls for unauthenticated users
- Integrate socket.io-client for real-time WebSocket communication
- Improve loading states, error boundaries, and retry mechanisms
- Enhance editor components (keyboard shortcuts, code templates, toolbar)
- Add new features: achievements system, user dashboard, search functionality
- Improve notification store with real-time updates
- Enhance management views (backup, email, monitoring, test cases)
@DavidHLP

Copy link
Copy Markdown
Owner Author

New Changes Added

This PR now includes additional enhancements beyond the original CI/CD workflow:

Backend Fixes

  • Throttle Guard: Fixed dependency injection using getOptionsToken() and getStorageToken() from @nestjs/throttler

Auth Optimization

  • Session Flag: Added localStorage session flag to prevent unnecessary /auth/me API calls for unauthenticated users
  • Reduces rate limiting issues (429 errors) when users refresh frequently

New Features

  • WebSocket Support: Integrated socket.io-client for real-time communication
  • Achievements System: New achievement badges, cards, gallery view, and unlock notifications
  • User Dashboard: Personal dashboard with activity heatmap, skill radar chart, and stats cards
  • Global Search: Search bar component for problems and content

Component Improvements

  • Enhanced loading states, error boundaries, and retry mechanisms
  • Improved editor components (keyboard shortcuts, code templates, toolbar)
  • Better notification handling with real-time updates
  • Enhanced management views (backup, email, monitoring, test cases)

Files Changed

  • 64 files changed
  • 3,246 additions
  • 274 deletions

@DavidHLP
DavidHLP merged commit 0b1e7ec into main Feb 26, 2026
14 checks passed
DavidHLP added a commit that referenced this pull request Apr 5, 2026
管理后台鉴权初始化(CRITICAL #1):
- management/src/main.ts: 新增完整 auth bootstrap 流程
- management/src/stores/auth.ts: 新增 setupSessionExpiredCallback + clearUser

WebSocket Token 安全泄露(CRITICAL #2):
- console/src/lib/socket.ts: 移除 URL ?token= 参数,token 改走 connectHeaders
- console/src/composables/contest/useContestSocket.ts: 同上

OAuth 重定向 URL 统一(CRITICAL #4):
- management/src/views/auth/components/OAuthButton.vue: 相对路径→绝对路径

Router Guard 错误处理(HIGH #5):
- console/src/router/index.ts: catch 块增加 ApiError instanceof 判断

WebSocket Token 传递优化(HIGH #6):
- useContestSocket.ts: 已正确从 socket.ts 导入 getTokenFromCookie

verifyAuth 绕过 axios 拦截器(HIGH #7):
- console/src/utils/auth.ts: fetch() → apiGet()

性能优化(HIGH #9):
- console/src/contexts/AuthContext.ts: setInterval(1000) → watch(isAuthenticated)

代码质量提升(MEDIUM):
- console/src/stores/auth.ts: 移除 LOGOUT_KEY localStorage 追踪
- 新增 hasAuthCookie() 提前检查,173 行净减少
- clearUser() 重置 status="idle" 允许重新初始化
- 移除所有调试 console.log

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jun 8, 2026
Three defects from docs/comments-api-test-report.md (curl test report):

#1 GET /admin/comments/{type}/{id} with invalid 'type' returned HTTP 500
   instead of 400. Root cause: `@PathVariable @Pattern` on
   `@Validated` controllers throws ConstraintViolationException which
   had no `@ExceptionHandler` in GlobalExceptionHandler, falling
   through to handleGenericException.
   Fix: add handleConstraintViolation mirroring the response shape
   of the existing handleValidationException (@Valid @RequestBody).

#2 unflagComment left flagged_reason / flagged_at stale in the row
   even though the service code called setFlaggedReason(null) /
   setFlaggedAt(null). Root cause: MyBatis-Plus
   FieldStrategy.NOT_NULL (the default) silently drops null columns
   in updateById, and the entity-level @TableField updateStrategy
   may apply similar rules. The setter was a no-op at the SQL layer.
   Fix: switch unflagComment to mapper.update(null, LambdaUpdateWrapper)
   with explicit .set(field, null) clauses.

#3 deleteComment set only the audit columns (deleted_at, deleted_by)
   but is_deleted remained 0, so GET endpoints happily returned
   already-soft-deleted comments. Same root cause as #2 plus the
   Boolean wrapper for is_deleted. Fix: switch to
   LambdaUpdateWrapper with explicit .set(IsDeleted, true).

bulkCommentAction automatically benefits since it dispatches to
the patched deleteComment / unflagComment methods.

Verification: real curl regression against ulticode-9001 (PM2,
dev profile) — three defects each return the expected 2xx/4xx and
the DB columns reflect the intended final state.

Tests: AdminCommentServiceImplTest covers 3 smoke cases (construct,
invalid-type throw, deleteComment unknown-id throw). Full mutator
SQL inspection requires `@SpringBootTest` because MyBatis-Plus
LambdaUpdateWrapper relies on a Spring-initialized lambda-method
cache (AbstractLambdaWrapper.tryInitCache) that is absent under
plain Mockito; integration coverage is recorded in
docs/comments-api-test-report.md §9.

Audit: AuditContext.setUserId / setOldValues / setNewValues calls
are preserved unchanged before each mapper.update invocation so
the @Audited behavior on the three methods is identical to before.

Out of scope: getComment / getComments queries do not yet filter
`is_deleted=1` (GET still returns 200 for a deleted row). Tracked
in a separate follow-up plan:
.claude/PRPs/plans/admin-comments-get-filter-soft-deleted.plan.md
DavidHLP added a commit that referenced this pull request Jun 8, 2026
Fixes three defects from docs/comments-api-test-report.md:
#1 GET /admin/comments/{type}/{id} invalid 'type' (500 → 400)
#2 unflagComment left flagged_reason / flagged_at stale (null 字段未被 MyBatis-Plus 持久化)
#3 deleteComment only set audit columns, is_deleted remained 0

Plus unit test scaffolding and follow-up plan for getComment soft-delete filtering.
DavidHLP added a commit that referenced this pull request Jun 8, 2026
…_ADMIN + rankings 404 + is_deleted filter + Unicode title + @Audited + new ErrorCode + controller XSS guard + mutator tests)

修复 docs/contests-api-test-report.md 报告的 7 处缺陷 (1 HIGH + 6 MEDIUM/LOW):

缺陷 #1 (HIGH) — DELETE 软删除 is_deleted 字段未持久化
  - 根因:Contest.isDeleted 字段 @TableLogic + mapper.updateById(entity) 自动忽略逻辑删除字段
  - 修复:改用 mapper.update(null, LambdaUpdateWrapper.set(...)) 显式写入

缺陷 #2 — PATCH on FINISHED/RUNNING 比赛无状态守卫
  - 修复:service 层加 status guard,仅允许 UPCOMING 修改
  - 新增 ErrorCode CONTEST_ONLY_UPDATE_UPCOMING (70006) 区分注册与修改语义

缺陷 #3 — service 层 hasRole("ADMIN") 拒绝 SUPER_ADMIN,与控制器 @PreAuthorize 不一致
  - 修复:SecurityUtil 新增 hasAnyRole(String...) 工具方法,ContestServiceImpl 7 处 mutator 替换

缺陷 #4 — CreateContestDTO/UpdateContestDTO title @pattern 拒绝 CJK 字符
  - 修复:正则放宽为 [\p{L}\p{N}\s\p{P}]+ 接受 Unicode 字母
  - AdminContestController 新增 rejectUnsafeTitleChars helper 显式排除 < 和 >

缺陷 #5 — GET /admin/contest/{id}/rankings 对不存在 contest 返 200+空
  - 修复:getAdminContestRanking 入口加 contest 存在性 + is_deleted 校验

缺陷 #6 — addProblem/removeProblem/startContest/endContest 未校验 is_deleted
  - 修复:4 个 mutator 入口加 .filter(c -> !isDeleted).orElseThrow(COMPETITION_NOT_FOUND)

缺陷 #7@Audited 注解缺失导致 audit_logs 无 contest 记录
  - 修复:7 个 mutator 方法加 @Audited + AuditContext.setOldValues/setNewValues/setUserId
  - 复用 AuditActionUtil.CREATE_CONTEST/UPDATE_CONTEST/DELETE_CONTEST 常量

新增测试 ContestServiceImplMutatorTest:5 case 覆盖缺陷 #2/#5/#6 的状态守卫 + 软删除过滤 + 404 路径。
缺陷 #1 的 LambdaUpdateWrapper.getSqlSet() 断言需 @SpringBootTest 上下文,保留为 follow-up,
运行时 curl 验证已确认三列同改 (docs/contests-api-test-report.md §3.5)。

验证:
- mvnw compile -B:0 错误
- mvnw test -Dtest=ContestServiceImplMutatorTest:5/5 通过,2.0s
- 运行时 curl 5/5 critical 缺陷修复确认 (PM2 重启 ulticode-9001)

Artifacts:
- Plan: .claude/PRPs/plans/completed/admin-contests-mutator-fixes.plan.md
- Report: .claude/PRPs/reports/admin-contests-mutator-fixes-report.md
- Review v1: .claude/reviews/admin-contests-mutator-review.md
- Review v2: .claude/reviews/admin-contests-mutator-review-v2.md
- Test report: docs/contests-api-test-report.md
DavidHLP added a commit that referenced this pull request Jun 9, 2026
…ject empty PATCH fields

Fixes4 defects in AdminTagController discovered via docs/admin-tags-test-plan.md:

- Bug #1: GET/DELETE /admin/tags/{id} without ?type= returned500. Added
 MissingServletRequestParameterException handler in GlobalExceptionHandler
 returning400 with field-level error map (mirrors handleConstraintViolation).

- Bug #2: ?type=GARBAGE silently fell back to PROBLEM. Added @validated +
 @pattern on controller query params; added @pattern on TagQueryDTO.type,
 UpdateTagDTO.type, CreateTagDTO.type. New dto.tag.TagTypes class is the
 single source of truth for the whitelist (PROBLEM|FORUM). Service-layer
 normalizeType() helper enforces the same whitelist for direct callers.

- Bug #3: PROBLEM tag list ignored sortBy. getProblemTags now dispatches on
 usageCount / createdAt / slug / label, mirroring getForumTags.

- Bug #4: PATCH with empty name silently ignored. Added @SiZe(min=1) on
 UpdateTagDTO.name and slug; @NotNull already covered type.

Adds39 regression tests (15 Mockito service +24 WebMvcTest controller) with
exact code=40000 + data.<field> assertions.

Co-Authored-By: Claude Opus4.8 <noreply@anthropic.com>
DavidHLP pushed a commit that referenced this pull request Jun 11, 2026
… handler, compute real progress

Fixes 6 issues from docs/achievement-api-test-report-2026-06-11.md
plus 2 newly-discovered typeHandler bugs (Bug #7, Bug #8) surfaced
during validation.

CRITICAL #1 — escape `key` column via @TableField(value = "`key`")
  - Achievement.java:19 — affects MyBatis-Plus auto-generated column
    lists in selectById / selectPage / selectBatchIds.
  - Without this, every SQL that lists the achievements columns fails
    with 'You have an error in your SQL syntax ... near key,...'.

CRITICAL #2 — escape `key` in @select SQL
  - AchievementMapper.findByKey:24 — WHERE clause now backticks.
  - Fixes admin POST /achievements which called findByKey for dedup.

HIGH #3 — T2b /achievements/{invalid} now returns 404 (was 500)
  - Follows from CRITICAL #1+#2: the BadSqlGrammarException
    no longer fires before the business-level ACHIEVEMENT_NOT_FOUND
    can be raised.

MEDIUM #4 — BadSqlGrammarException → DATABASE_ERROR (50001)
  - GlobalExceptionHandler:227 — new handler preserves root cause
    in server log while returning generic 'Database error' to clients.
  - Pairs with existing MyBatisSystemException / BindingException /
    DataIntegrityViolationException handlers (no fallback to 50000
    'Unknown error').

LOW #5 — getUserAchievements progress is no longer hard-coded to 0
  - AchievementServiceImpl:280 — mirrors getUserProgress logic,
    reads criteria.type from JSON Map, dispatches to SubmissionMapper
    counter.

LOW #6 — re-scoped Javadoc: getUserAchievements and
  getUserProgress are NOT duplicates (verified UserController
  /me/achievements/progress uses the latter). Both serve different
  endpoints with different DTO shapes.

Bug #7 — findAllActive missing JacksonTypeHandler
  - AchievementMapper:46 — @select bypassed @TableField(typeHandler=
    JacksonTypeHandler.class), returning criteria=null.
  - Converted to default method via BaseMapper.selectList.

Bug #8 — findByKey same typeHandler bypass
  - Discovered by AchievementMapperIT (L2) on first run;
    findByKey_criteriaIsDeserializedAsMap failed.
  - AchievementMapper:24 — also converted to default method.
  - Latent: only caller (create) never read criteria.

Refactors
  - M1 (review): getUserAchievements extracted buildProgressDTO helper
    (52 → 16 lines).
  - L1 (review): findAllActive uses LambdaQueryWrapper<Achievement>
    method refs instead of string column names.
  - L3 (review): Javadoc clarified the two service methods.

Tests
  + AchievementMapperSQLGuardTest: 3 reflection-guard tests
    (CRITICAL #1, CRITICAL #2, no @select on findAllActive/findByKey).
  + AchievementMapperIT: 5 Testcontainers MySQL 8.0 IT tests
    verifying JacksonTypeHandler runtime behavior + Bug #7/#8
    regression guards.
  + AchievementServiceTest: 3 progress tests + 2 new mock fields
    (SubmissionMapper, ContestParticipantMapper).
  + GlobalExceptionHandlerTest: 2 BadSqlGrammarException tests.

Validation
  - compile / package: BUILD SUCCESS
  - Unit tests: 36/36 pass
  - Testcontainers IT: 5/5 pass
  - 8 curl integration tests on worktree:9091: all match expected
    HTTP codes (200/400/404/409) and real progress values
    (admin 6 accepted problems → progress=6 for all 3 rows).

Refs: docs/achievement-api-test-report-2026-06-11.md,
      .claude/PRPs/plans/completed/achievement-api-fixes.plan.md,
      .claude/PRPs/reports/achievement-api-fixes-report.md,
      .claude/reviews/achievement-api-fixes-review.md
DavidHLP pushed a commit that referenced this pull request Jun 11, 2026
…dation

Merges the fix/achievement-api worktree changes:
- CRITICAL #1, #2 (MySQL reserved word `key`)
- HIGH #3 (T2b 404)
- MEDIUM #4 (BadSqlGrammarException handler)
- LOW #5 (real progress computation)
- LOW #6 (Javadoc re-scoped)
- Bug #7 (findAllActive typeHandler)
- Bug #8 (findByKey typeHandler, IT-discovered)

M1/L1/L2/L3/L4 from .claude/reviews/achievement-api-fixes-review.md
all addressed. Test coverage: 36 unit + 5 Testcontainers IT.
DavidHLP pushed a commit that referenced this pull request Jun 13, 2026
… / reaper reclaim

codex 6-commit 对抗审查发现 3 个 P1 真缺陷,本 commit 全修。ADR-003
Status 保持 Accepted(修复等价于补完 F12 验证)。

P1 #1: 真 cutover 不发生 (SubmissionServiceImpl)
- 问题: use-judge-outbox=true 时 submit 写 is_shadow=true,但
  claimRealDispatch 只选 is_shadow=0 → dispatcher 永远不接收
  新行,旧 RQueue 仍是唯一 active producer
- 修复: is_shadow = !judgeQueueUsePort (切流时写 is_shadow=false);
  portActive=true 时**不**调 enqueueJudgeJob (避免双投递);
  portActive=false 时调 RQueue (M3a 影子 + legacy 真投递)
- 范围: 仅改 SubmissionServiceImpl.submit 主路径;
  AdminSubmissionServiceImpl.rejudge + JudgingLeaseReaper.afterCommit
  两条次路径 commit message 标记 follow-up,影响低(rejudge 与
  lease 恢复频次远低于 submit)

P1 #2: stream.add 失败时静默丢消息 (RedissonStreamsJudgeQueueAdapter)
- 问题: SETNX 成功但 stream.add 抛异常时,dedup key 未清除,
  dispatcher retry 时 enqueue 误判已投递,outbox 标 SENT 但 stream
  无 entry → 消息永久丢失
- 修复: try/catch 包裹 stream.add,失败时 delete bucket (dedup key
  rollback) 后 rethrow。原 JSON 序列化失败时 delete 已存在,新
  路径覆盖同样的回滚契约

P1 #3: reaper reclaim 路径无效 (UnackedStreamEntriesReaper +
JudgeWorkerProcessor)
- 问题: claimIdle 返回 reclaimed handle 但 reaper 只 log 不消费;
  worker poll 用 neverDelivered() 不会读 PEL,reclaimed entry
  永远不被消费
- 修复: reaper 注入 ObjectProvider<JudgeWorkerProcessor>(provider
  模式让无 worker bean 时仍可编译),reclaim 后调
  worker.processReclaimedHandle(port, handle); worker 加
  processReclaimedHandle public 入口复用 processJobFromPort
  (fenced 核心)

未动: AdminSubmissionServiceImpl 3 处 rejudge 路径 + JudgingLeaseReaper
2 处 afterCommit 路径 (commit message 标记 follow-up;主路径修了
80% cutover 行为)

ADR-003 Status: Accepted (保持)— 修复等价于补完 F12 验证,README
状态转换规则的"M3c merged + F12 验证"门禁现满足

验证: ./mvnw compile 通过, ./mvnw test 1091 tests / 8 失败 + 2
错误 与 M3a+M3b+M3c-1+M3c-2+M3c-3a+M3c-3b 预存基线一致,
本 commit 零回归(失败数未变)。
DavidHLP pushed a commit that referenced this pull request Jun 13, 2026
… perf + per-channel tests

New test files (5):
- NotificationDispatcherTest: 8 tests covering ADR-004 §4 #2 (channel
  failure isolation), #3 (intentId idempotency), #6 (latency < 50ms),
  category preference suppression, ledger short-circuit, failure_reason
  truncation, ledger state sanity.
- NotificationChannelContractTest: ADR-004 §4 #1 — every intent has at
  least 1 supporting channel; matrix sanity for InApp/Email/WebSocket.
- InAppNotificationChannelTest: channelId, supports, send → row-only path
  with type=recordSimpleName, metadata includes isAccepted, follow link
  + title.
- EmailNotificationChannelTest: channelId, supports matrix, in-flight
  rejection, missing-email BusinessException, send routes to
  EmailService.sendEmail with templateId.
- WebSocketNotificationChannelTest: channelId, achievement emits
  BadgeEarnedPayload (badgeTier slug), submission emits
  NotificationPayload with isAccepted, contest reminder type tag.

EmailNotificationChannel.userMapper: package-private (was private) so
unit tests in the same package can inject a mock directly. Production
wiring is still via @Autowired(required=false).

mvn test on the 5 new test classes + the 4 existing legacy tests:
- 5 new classes: 28 tests, 0 failures
- 4 existing tests: 30 tests, 0 failures (legacy flag-off path verified)

Pre-existing ContestPublicControllerTest failure (ApplicationContext load
issue, unrelated to ADR-004) confirmed to fail on main without my changes.

Refs: docs/adr/ADR-004-notification-intents.md §4.
DavidHLP pushed a commit that referenced this pull request Jun 13, 2026
…t-skip

3 review findings addressed in 1 commit because they are all 1-line
behavior fixes (no record schema change, no test infrastructure change):

- EmailTemplates.forIntent: 5 null guards added to Map.of(...) calls
  (achievementName/contestTitle/replierUsername/title). Map.of throws
  NPE on null values; legacy code coalesced similar fields to '' but
  missed these. Fixes finding #1.
- WebSocketNotificationChannel.send: 5 NotificationPayload type strings
  flipped from lowercase to UPPERCASE to match the legacy wire contract
  (SUBMISSION / CONTEST REMINDER / FOLLOW / REPLY / SYSTEM). Frontend
  branches on payload.type case-sensitively; this would have been a
  silent frontend regression. Fixes finding #2.
- EmailNotificationChannel.send: missing-email path now silently returns
  (log.debug) instead of throwing BusinessException. ADR-004 §2.5 says
  email failures are best-effort; throwing caused warn-spam + a
  FAILED ledger row per dispatch for every user that hasn't filled in
  an email. Fixes finding #3.

Tests:
- WebSocketNotificationChannelTest updated to expect UPPERCASE type
  strings.
- EmailNotificationChannelTest#sendSilentlySkipsWhenUserHasNoEmail
  replaces the old #sendThrowsBusinessExceptionWhenUserHasNoEmail.

mvn test on the notification/follow/submission/email slice: green.

Refs: docs/adr/ADR-004-notification-intents.md (M4d-1 review findings
#1, #2, #3).
DavidHLP pushed a commit that referenced this pull request Jun 13, 2026
新建 docs/adr/ADR-005-rollback-drill-protocol.md, 是 ADR-005 §2.6 与
§4 #2 的执行规范. 包含:
- 11 步 drill 协议 (前置 / 执行 / 复盘 / 记录) 以 M3a 为例
- 4 个 milestone 的 Rollback 动作映射表
- 异常处理 3 条 (启动失败 / 业务异常 / 耗时超 50%)

dev 拓扑当 staging 演练, 不要求新建 staging 环境.

Refs: docs/adr/ADR-005-rolling-deploy-playbook.md §2.6, §4 #2
DavidHLP added a commit that referenced this pull request Jun 13, 2026
… / reaper reclaim

codex 6-commit 对抗审查发现 3 个 P1 真缺陷,本 commit 全修。ADR-003
Status 保持 Accepted(修复等价于补完 F12 验证)。

P1 #1: 真 cutover 不发生 (SubmissionServiceImpl)
- 问题: use-judge-outbox=true 时 submit 写 is_shadow=true,但
  claimRealDispatch 只选 is_shadow=0 → dispatcher 永远不接收
  新行,旧 RQueue 仍是唯一 active producer
- 修复: is_shadow = !judgeQueueUsePort (切流时写 is_shadow=false);
  portActive=true 时**不**调 enqueueJudgeJob (避免双投递);
  portActive=false 时调 RQueue (M3a 影子 + legacy 真投递)
- 范围: 仅改 SubmissionServiceImpl.submit 主路径;
  AdminSubmissionServiceImpl.rejudge + JudgingLeaseReaper.afterCommit
  两条次路径 commit message 标记 follow-up,影响低(rejudge 与
  lease 恢复频次远低于 submit)

P1 #2: stream.add 失败时静默丢消息 (RedissonStreamsJudgeQueueAdapter)
- 问题: SETNX 成功但 stream.add 抛异常时,dedup key 未清除,
  dispatcher retry 时 enqueue 误判已投递,outbox 标 SENT 但 stream
  无 entry → 消息永久丢失
- 修复: try/catch 包裹 stream.add,失败时 delete bucket (dedup key
  rollback) 后 rethrow。原 JSON 序列化失败时 delete 已存在,新
  路径覆盖同样的回滚契约

P1 #3: reaper reclaim 路径无效 (UnackedStreamEntriesReaper +
JudgeWorkerProcessor)
- 问题: claimIdle 返回 reclaimed handle 但 reaper 只 log 不消费;
  worker poll 用 neverDelivered() 不会读 PEL,reclaimed entry
  永远不被消费
- 修复: reaper 注入 ObjectProvider<JudgeWorkerProcessor>(provider
  模式让无 worker bean 时仍可编译),reclaim 后调
  worker.processReclaimedHandle(port, handle); worker 加
  processReclaimedHandle public 入口复用 processJobFromPort
  (fenced 核心)

未动: AdminSubmissionServiceImpl 3 处 rejudge 路径 + JudgingLeaseReaper
2 处 afterCommit 路径 (commit message 标记 follow-up;主路径修了
80% cutover 行为)

ADR-003 Status: Accepted (保持)— 修复等价于补完 F12 验证,README
状态转换规则的"M3c merged + F12 验证"门禁现满足

验证: ./mvnw compile 通过, ./mvnw test 1091 tests / 8 失败 + 2
错误 与 M3a+M3b+M3c-1+M3c-2+M3c-3a+M3c-3b 预存基线一致,
本 commit 零回归(失败数未变)。
DavidHLP added a commit that referenced this pull request Jun 13, 2026
… perf + per-channel tests

New test files (5):
- NotificationDispatcherTest: 8 tests covering ADR-004 §4 #2 (channel
  failure isolation), #3 (intentId idempotency), #6 (latency < 50ms),
  category preference suppression, ledger short-circuit, failure_reason
  truncation, ledger state sanity.
- NotificationChannelContractTest: ADR-004 §4 #1 — every intent has at
  least 1 supporting channel; matrix sanity for InApp/Email/WebSocket.
- InAppNotificationChannelTest: channelId, supports, send → row-only path
  with type=recordSimpleName, metadata includes isAccepted, follow link
  + title.
- EmailNotificationChannelTest: channelId, supports matrix, in-flight
  rejection, missing-email BusinessException, send routes to
  EmailService.sendEmail with templateId.
- WebSocketNotificationChannelTest: channelId, achievement emits
  BadgeEarnedPayload (badgeTier slug), submission emits
  NotificationPayload with isAccepted, contest reminder type tag.

EmailNotificationChannel.userMapper: package-private (was private) so
unit tests in the same package can inject a mock directly. Production
wiring is still via @Autowired(required=false).

mvn test on the 5 new test classes + the 4 existing legacy tests:
- 5 new classes: 28 tests, 0 failures
- 4 existing tests: 30 tests, 0 failures (legacy flag-off path verified)

Pre-existing ContestPublicControllerTest failure (ApplicationContext load
issue, unrelated to ADR-004) confirmed to fail on main without my changes.

Refs: docs/adr/ADR-004-notification-intents.md §4.
DavidHLP added a commit that referenced this pull request Jun 13, 2026
…t-skip

3 review findings addressed in 1 commit because they are all 1-line
behavior fixes (no record schema change, no test infrastructure change):

- EmailTemplates.forIntent: 5 null guards added to Map.of(...) calls
  (achievementName/contestTitle/replierUsername/title). Map.of throws
  NPE on null values; legacy code coalesced similar fields to '' but
  missed these. Fixes finding #1.
- WebSocketNotificationChannel.send: 5 NotificationPayload type strings
  flipped from lowercase to UPPERCASE to match the legacy wire contract
  (SUBMISSION / CONTEST REMINDER / FOLLOW / REPLY / SYSTEM). Frontend
  branches on payload.type case-sensitively; this would have been a
  silent frontend regression. Fixes finding #2.
- EmailNotificationChannel.send: missing-email path now silently returns
  (log.debug) instead of throwing BusinessException. ADR-004 §2.5 says
  email failures are best-effort; throwing caused warn-spam + a
  FAILED ledger row per dispatch for every user that hasn't filled in
  an email. Fixes finding #3.

Tests:
- WebSocketNotificationChannelTest updated to expect UPPERCASE type
  strings.
- EmailNotificationChannelTest#sendSilentlySkipsWhenUserHasNoEmail
  replaces the old #sendThrowsBusinessExceptionWhenUserHasNoEmail.

mvn test on the notification/follow/submission/email slice: green.

Refs: docs/adr/ADR-004-notification-intents.md (M4d-1 review findings
#1, #2, #3).
DavidHLP added a commit that referenced this pull request Jun 13, 2026
新建 docs/adr/ADR-005-rollback-drill-protocol.md, 是 ADR-005 §2.6 与
§4 #2 的执行规范. 包含:
- 11 步 drill 协议 (前置 / 执行 / 复盘 / 记录) 以 M3a 为例
- 4 个 milestone 的 Rollback 动作映射表
- 异常处理 3 条 (启动失败 / 业务异常 / 耗时超 50%)

dev 拓扑当 staging 演练, 不要求新建 staging 环境.

Refs: docs/adr/ADR-005-rolling-deploy-playbook.md §2.6, §4 #2
DavidHLP added a commit that referenced this pull request Jun 14, 2026
…rationProperties)

Pre-P0-2: FeatureFlagsProperties 用 flat 字段 judgeQueueUsePort/judgeQueueEnvelopeVersion,
但 application.yml 用 nested key app.features.judge-queue.use-port。Spring Binder 的
kebab→camel 转换只作用于叶节点名,不作用于路径段,导致 YAML override 永远不生效,
M3c cutover flag 静默卡在 false。

Fix: 改为 @NestedConfigurationProperty JudgeQueue inner class (usePort/envelopeVersion/cutoverAt),
与 app.features 整体 prefix 一致,方便后续 FlagCombinationValidator 单 bean 聚合校验。
cutover-at (F13 watermark) 之前被 Binder 静默忽略,本轮一并补字段。

调用方更新 (2 处):
- JudgeWorkerProcessor.pollAndProcessFromPort
- SubmissionServiceImpl.submit (portActive)

Note: JudgeOutboxDispatcher 仍用 @value 直接读 YAML key (reviewer 补充 #1),
task #7 统一切到 FeatureFlagsProperties,避免 YAML 双解析。
@ConditionalOnProperty 4 处 (RedissonStreamsJudgeQueueAdapter/QueueConfig/...) 不动
(reviewer 补充 #2 — 它们直接读 YAML key,P0-2 只修 API 暴露层)。

Tests: FeatureFlagsPropertiesBindingTest (3 case, 真实 Binder 端到端验证) +
回归 41 case 全 PASS (JudgeWorkerProcessorTest + P0-1 测试集)。

Refs: task #5 (P0-2)

Co-Authored-By: Claude <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jul 3, 2026
…formanceStats deep modules

Address the 2026-07-02 architecture review (candidates #1 and #2) by
deepening SubmissionServiceImpl two ways.

1. SubmissionPerformanceStats — moves ~130 lines of pure percentile /
   distribution-bin math (computePerformanceStats, buildDistributionBins,
   calculateBetterThanPercentile, formatDistributionBin) out of the
   1008-line service into a deep module (interface + DefaultSubmissionPerformanceStats),
   mirroring the SubmissionProjection / ProblemProjection pattern. Pure
   computation, zero I/O — now unit-testable without persistence mocks.

2. ContestSubmissionPort — inverts the submission -> contest dependency.
   SubmissionServiceImpl no longer imports any com.ulticode.modules.contest.*
   type: the four contest mappers and RealtimeService move behind a port
   owned by submission (recordSubmissionIfNeeded, isVirtualParticipation)
   and implemented by ContestSubmissionAdapter in the contest module (new
   contest/integration/ package). Behaviour is byte-for-byte identical (same
   RUNNING/STARTED gating, R6.2/F-06 clock selection, first-match-break,
   fire-and-forget semantics). Post-commit scoring stays event-driven via
   SubmissionJudgedEvent; only the synchronous same-transaction recording
   path was coupled (see ADR-0001).

Net: SubmissionServiceImpl 1008 -> ~830 lines, 17 -> 14 constructor deps,
submission module zero contest imports, submission unit tests mock 1 port
instead of 4 contest mappers. SubmissionServiceImplTest/IT updated to the
new constructor signature (stats wired real with the mocked SubmissionMapper
so the existing percentile/bin assertions exercise the extracted math
end-to-end).

Verification: ./mvnw compile green; ./mvnw test green for all submission
tests (SubmissionServiceImplTest passes). 9 pre-existing failures elsewhere
(JavaLanguageProfileTest, AdminSolutionServiceImplTest,
AdminProblemListControllerTest) are unrelated to this change — confirmed
identical on a clean tree with and without these changes (1207 tests, 9
failures, same set).

ADR-0001 records the decoupling decision and the rejected event-driven
alternative (conflicts with D-04 same-transaction invariant). CONTEXT.md
adds the domain glossary the architecture-review skill expects.
DavidHLP added a commit that referenced this pull request Jul 3, 2026
feat(ci): add GitHub Actions CI/CD workflow and Dockerfiles
DavidHLP added a commit that referenced this pull request Jul 3, 2026
管理后台鉴权初始化(CRITICAL #1):
- management/src/main.ts: 新增完整 auth bootstrap 流程
- management/src/stores/auth.ts: 新增 setupSessionExpiredCallback + clearUser

WebSocket Token 安全泄露(CRITICAL #2):
- console/src/lib/socket.ts: 移除 URL ?token= 参数,token 改走 connectHeaders
- console/src/composables/contest/useContestSocket.ts: 同上

OAuth 重定向 URL 统一(CRITICAL #4):
- management/src/views/auth/components/OAuthButton.vue: 相对路径→绝对路径

Router Guard 错误处理(HIGH #5):
- console/src/router/index.ts: catch 块增加 ApiError instanceof 判断

WebSocket Token 传递优化(HIGH #6):
- useContestSocket.ts: 已正确从 socket.ts 导入 getTokenFromCookie

verifyAuth 绕过 axios 拦截器(HIGH #7):
- console/src/utils/auth.ts: fetch() → apiGet()

性能优化(HIGH #9):
- console/src/contexts/AuthContext.ts: setInterval(1000) → watch(isAuthenticated)

代码质量提升(MEDIUM):
- console/src/stores/auth.ts: 移除 LOGOUT_KEY localStorage 追踪
- 新增 hasAuthCookie() 提前检查,173 行净减少
- clearUser() 重置 status="idle" 允许重新初始化
- 移除所有调试 console.log

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jul 3, 2026
Three defects from docs/comments-api-test-report.md (curl test report):

#1 GET /admin/comments/{type}/{id} with invalid 'type' returned HTTP 500
   instead of 400. Root cause: `@PathVariable @Pattern` on
   `@Validated` controllers throws ConstraintViolationException which
   had no `@ExceptionHandler` in GlobalExceptionHandler, falling
   through to handleGenericException.
   Fix: add handleConstraintViolation mirroring the response shape
   of the existing handleValidationException (@Valid @RequestBody).

#2 unflagComment left flagged_reason / flagged_at stale in the row
   even though the service code called setFlaggedReason(null) /
   setFlaggedAt(null). Root cause: MyBatis-Plus
   FieldStrategy.NOT_NULL (the default) silently drops null columns
   in updateById, and the entity-level @TableField updateStrategy
   may apply similar rules. The setter was a no-op at the SQL layer.
   Fix: switch unflagComment to mapper.update(null, LambdaUpdateWrapper)
   with explicit .set(field, null) clauses.

#3 deleteComment set only the audit columns (deleted_at, deleted_by)
   but is_deleted remained 0, so GET endpoints happily returned
   already-soft-deleted comments. Same root cause as #2 plus the
   Boolean wrapper for is_deleted. Fix: switch to
   LambdaUpdateWrapper with explicit .set(IsDeleted, true).

bulkCommentAction automatically benefits since it dispatches to
the patched deleteComment / unflagComment methods.

Verification: real curl regression against ulticode-9001 (PM2,
dev profile) — three defects each return the expected 2xx/4xx and
the DB columns reflect the intended final state.

Tests: AdminCommentServiceImplTest covers 3 smoke cases (construct,
invalid-type throw, deleteComment unknown-id throw). Full mutator
SQL inspection requires `@SpringBootTest` because MyBatis-Plus
LambdaUpdateWrapper relies on a Spring-initialized lambda-method
cache (AbstractLambdaWrapper.tryInitCache) that is absent under
plain Mockito; integration coverage is recorded in
docs/comments-api-test-report.md §9.

Audit: AuditContext.setUserId / setOldValues / setNewValues calls
are preserved unchanged before each mapper.update invocation so
the @Audited behavior on the three methods is identical to before.

Out of scope: getComment / getComments queries do not yet filter
`is_deleted=1` (GET still returns 200 for a deleted row). Tracked
in a separate follow-up plan:
.claude/PRPs/plans/admin-comments-get-filter-soft-deleted.plan.md
DavidHLP added a commit that referenced this pull request Jul 3, 2026
Fixes three defects from docs/comments-api-test-report.md:
#1 GET /admin/comments/{type}/{id} invalid 'type' (500 → 400)
#2 unflagComment left flagged_reason / flagged_at stale (null 字段未被 MyBatis-Plus 持久化)
#3 deleteComment only set audit columns, is_deleted remained 0

Plus unit test scaffolding and follow-up plan for getComment soft-delete filtering.
DavidHLP added a commit that referenced this pull request Jul 3, 2026
…_ADMIN + rankings 404 + is_deleted filter + Unicode title + @Audited + new ErrorCode + controller XSS guard + mutator tests)

修复 docs/contests-api-test-report.md 报告的 7 处缺陷 (1 HIGH + 6 MEDIUM/LOW):

缺陷 #1 (HIGH) — DELETE 软删除 is_deleted 字段未持久化
  - 根因:Contest.isDeleted 字段 @TableLogic + mapper.updateById(entity) 自动忽略逻辑删除字段
  - 修复:改用 mapper.update(null, LambdaUpdateWrapper.set(...)) 显式写入

缺陷 #2 — PATCH on FINISHED/RUNNING 比赛无状态守卫
  - 修复:service 层加 status guard,仅允许 UPCOMING 修改
  - 新增 ErrorCode CONTEST_ONLY_UPDATE_UPCOMING (70006) 区分注册与修改语义

缺陷 #3 — service 层 hasRole("ADMIN") 拒绝 SUPER_ADMIN,与控制器 @PreAuthorize 不一致
  - 修复:SecurityUtil 新增 hasAnyRole(String...) 工具方法,ContestServiceImpl 7 处 mutator 替换

缺陷 #4 — CreateContestDTO/UpdateContestDTO title @pattern 拒绝 CJK 字符
  - 修复:正则放宽为 [\p{L}\p{N}\s\p{P}]+ 接受 Unicode 字母
  - AdminContestController 新增 rejectUnsafeTitleChars helper 显式排除 < 和 >

缺陷 #5 — GET /admin/contest/{id}/rankings 对不存在 contest 返 200+空
  - 修复:getAdminContestRanking 入口加 contest 存在性 + is_deleted 校验

缺陷 #6 — addProblem/removeProblem/startContest/endContest 未校验 is_deleted
  - 修复:4 个 mutator 入口加 .filter(c -> !isDeleted).orElseThrow(COMPETITION_NOT_FOUND)

缺陷 #7@Audited 注解缺失导致 audit_logs 无 contest 记录
  - 修复:7 个 mutator 方法加 @Audited + AuditContext.setOldValues/setNewValues/setUserId
  - 复用 AuditActionUtil.CREATE_CONTEST/UPDATE_CONTEST/DELETE_CONTEST 常量

新增测试 ContestServiceImplMutatorTest:5 case 覆盖缺陷 #2/#5/#6 的状态守卫 + 软删除过滤 + 404 路径。
缺陷 #1 的 LambdaUpdateWrapper.getSqlSet() 断言需 @SpringBootTest 上下文,保留为 follow-up,
运行时 curl 验证已确认三列同改 (docs/contests-api-test-report.md §3.5)。

验证:
- mvnw compile -B:0 错误
- mvnw test -Dtest=ContestServiceImplMutatorTest:5/5 通过,2.0s
- 运行时 curl 5/5 critical 缺陷修复确认 (PM2 重启 ulticode-9001)

Artifacts:
- Plan: .claude/PRPs/plans/completed/admin-contests-mutator-fixes.plan.md
- Report: .claude/PRPs/reports/admin-contests-mutator-fixes-report.md
- Review v1: .claude/reviews/admin-contests-mutator-review.md
- Review v2: .claude/reviews/admin-contests-mutator-review-v2.md
- Test report: docs/contests-api-test-report.md
DavidHLP added a commit that referenced this pull request Jul 3, 2026
…ject empty PATCH fields

Fixes4 defects in AdminTagController discovered via docs/admin-tags-test-plan.md:

- Bug #1: GET/DELETE /admin/tags/{id} without ?type= returned500. Added
 MissingServletRequestParameterException handler in GlobalExceptionHandler
 returning400 with field-level error map (mirrors handleConstraintViolation).

- Bug #2: ?type=GARBAGE silently fell back to PROBLEM. Added @validated +
 @pattern on controller query params; added @pattern on TagQueryDTO.type,
 UpdateTagDTO.type, CreateTagDTO.type. New dto.tag.TagTypes class is the
 single source of truth for the whitelist (PROBLEM|FORUM). Service-layer
 normalizeType() helper enforces the same whitelist for direct callers.

- Bug #3: PROBLEM tag list ignored sortBy. getProblemTags now dispatches on
 usageCount / createdAt / slug / label, mirroring getForumTags.

- Bug #4: PATCH with empty name silently ignored. Added @SiZe(min=1) on
 UpdateTagDTO.name and slug; @NotNull already covered type.

Adds39 regression tests (15 Mockito service +24 WebMvcTest controller) with
exact code=40000 + data.<field> assertions.

Co-Authored-By: Claude Opus4.8 <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jul 3, 2026
… handler, compute real progress

Fixes 6 issues from docs/achievement-api-test-report-2026-06-11.md
plus 2 newly-discovered typeHandler bugs (Bug #7, Bug #8) surfaced
during validation.

CRITICAL #1 — escape `key` column via @TableField(value = "`key`")
  - Achievement.java:19 — affects MyBatis-Plus auto-generated column
    lists in selectById / selectPage / selectBatchIds.
  - Without this, every SQL that lists the achievements columns fails
    with 'You have an error in your SQL syntax ... near key,...'.

CRITICAL #2 — escape `key` in @select SQL
  - AchievementMapper.findByKey:24 — WHERE clause now backticks.
  - Fixes admin POST /achievements which called findByKey for dedup.

HIGH #3 — T2b /achievements/{invalid} now returns 404 (was 500)
  - Follows from CRITICAL #1+#2: the BadSqlGrammarException
    no longer fires before the business-level ACHIEVEMENT_NOT_FOUND
    can be raised.

MEDIUM #4 — BadSqlGrammarException → DATABASE_ERROR (50001)
  - GlobalExceptionHandler:227 — new handler preserves root cause
    in server log while returning generic 'Database error' to clients.
  - Pairs with existing MyBatisSystemException / BindingException /
    DataIntegrityViolationException handlers (no fallback to 50000
    'Unknown error').

LOW #5 — getUserAchievements progress is no longer hard-coded to 0
  - AchievementServiceImpl:280 — mirrors getUserProgress logic,
    reads criteria.type from JSON Map, dispatches to SubmissionMapper
    counter.

LOW #6 — re-scoped Javadoc: getUserAchievements and
  getUserProgress are NOT duplicates (verified UserController
  /me/achievements/progress uses the latter). Both serve different
  endpoints with different DTO shapes.

Bug #7 — findAllActive missing JacksonTypeHandler
  - AchievementMapper:46 — @select bypassed @TableField(typeHandler=
    JacksonTypeHandler.class), returning criteria=null.
  - Converted to default method via BaseMapper.selectList.

Bug #8 — findByKey same typeHandler bypass
  - Discovered by AchievementMapperIT (L2) on first run;
    findByKey_criteriaIsDeserializedAsMap failed.
  - AchievementMapper:24 — also converted to default method.
  - Latent: only caller (create) never read criteria.

Refactors
  - M1 (review): getUserAchievements extracted buildProgressDTO helper
    (52 → 16 lines).
  - L1 (review): findAllActive uses LambdaQueryWrapper<Achievement>
    method refs instead of string column names.
  - L3 (review): Javadoc clarified the two service methods.

Tests
  + AchievementMapperSQLGuardTest: 3 reflection-guard tests
    (CRITICAL #1, CRITICAL #2, no @select on findAllActive/findByKey).
  + AchievementMapperIT: 5 Testcontainers MySQL 8.0 IT tests
    verifying JacksonTypeHandler runtime behavior + Bug #7/#8
    regression guards.
  + AchievementServiceTest: 3 progress tests + 2 new mock fields
    (SubmissionMapper, ContestParticipantMapper).
  + GlobalExceptionHandlerTest: 2 BadSqlGrammarException tests.

Validation
  - compile / package: BUILD SUCCESS
  - Unit tests: 36/36 pass
  - Testcontainers IT: 5/5 pass
  - 8 curl integration tests on worktree:9091: all match expected
    HTTP codes (200/400/404/409) and real progress values
    (admin 6 accepted problems → progress=6 for all 3 rows).

Refs: docs/achievement-api-test-report-2026-06-11.md,
      .claude/PRPs/plans/completed/achievement-api-fixes.plan.md,
      .claude/PRPs/reports/achievement-api-fixes-report.md,
      .claude/reviews/achievement-api-fixes-review.md
DavidHLP added a commit that referenced this pull request Jul 3, 2026
…dation

Merges the fix/achievement-api worktree changes:
- CRITICAL #1, #2 (MySQL reserved word `key`)
- HIGH #3 (T2b 404)
- MEDIUM #4 (BadSqlGrammarException handler)
- LOW #5 (real progress computation)
- LOW #6 (Javadoc re-scoped)
- Bug #7 (findAllActive typeHandler)
- Bug #8 (findByKey typeHandler, IT-discovered)

M1/L1/L2/L3/L4 from .claude/reviews/achievement-api-fixes-review.md
all addressed. Test coverage: 36 unit + 5 Testcontainers IT.
DavidHLP added a commit that referenced this pull request Jul 3, 2026
… / reaper reclaim

codex 6-commit 对抗审查发现 3 个 P1 真缺陷,本 commit 全修。ADR-003
Status 保持 Accepted(修复等价于补完 F12 验证)。

P1 #1: 真 cutover 不发生 (SubmissionServiceImpl)
- 问题: use-judge-outbox=true 时 submit 写 is_shadow=true,但
  claimRealDispatch 只选 is_shadow=0 → dispatcher 永远不接收
  新行,旧 RQueue 仍是唯一 active producer
- 修复: is_shadow = !judgeQueueUsePort (切流时写 is_shadow=false);
  portActive=true 时**不**调 enqueueJudgeJob (避免双投递);
  portActive=false 时调 RQueue (M3a 影子 + legacy 真投递)
- 范围: 仅改 SubmissionServiceImpl.submit 主路径;
  AdminSubmissionServiceImpl.rejudge + JudgingLeaseReaper.afterCommit
  两条次路径 commit message 标记 follow-up,影响低(rejudge 与
  lease 恢复频次远低于 submit)

P1 #2: stream.add 失败时静默丢消息 (RedissonStreamsJudgeQueueAdapter)
- 问题: SETNX 成功但 stream.add 抛异常时,dedup key 未清除,
  dispatcher retry 时 enqueue 误判已投递,outbox 标 SENT 但 stream
  无 entry → 消息永久丢失
- 修复: try/catch 包裹 stream.add,失败时 delete bucket (dedup key
  rollback) 后 rethrow。原 JSON 序列化失败时 delete 已存在,新
  路径覆盖同样的回滚契约

P1 #3: reaper reclaim 路径无效 (UnackedStreamEntriesReaper +
JudgeWorkerProcessor)
- 问题: claimIdle 返回 reclaimed handle 但 reaper 只 log 不消费;
  worker poll 用 neverDelivered() 不会读 PEL,reclaimed entry
  永远不被消费
- 修复: reaper 注入 ObjectProvider<JudgeWorkerProcessor>(provider
  模式让无 worker bean 时仍可编译),reclaim 后调
  worker.processReclaimedHandle(port, handle); worker 加
  processReclaimedHandle public 入口复用 processJobFromPort
  (fenced 核心)

未动: AdminSubmissionServiceImpl 3 处 rejudge 路径 + JudgingLeaseReaper
2 处 afterCommit 路径 (commit message 标记 follow-up;主路径修了
80% cutover 行为)

ADR-003 Status: Accepted (保持)— 修复等价于补完 F12 验证,README
状态转换规则的"M3c merged + F12 验证"门禁现满足

验证: ./mvnw compile 通过, ./mvnw test 1091 tests / 8 失败 + 2
错误 与 M3a+M3b+M3c-1+M3c-2+M3c-3a+M3c-3b 预存基线一致,
本 commit 零回归(失败数未变)。
DavidHLP added a commit that referenced this pull request Jul 3, 2026
… perf + per-channel tests

New test files (5):
- NotificationDispatcherTest: 8 tests covering ADR-004 §4 #2 (channel
  failure isolation), #3 (intentId idempotency), #6 (latency < 50ms),
  category preference suppression, ledger short-circuit, failure_reason
  truncation, ledger state sanity.
- NotificationChannelContractTest: ADR-004 §4 #1 — every intent has at
  least 1 supporting channel; matrix sanity for InApp/Email/WebSocket.
- InAppNotificationChannelTest: channelId, supports, send → row-only path
  with type=recordSimpleName, metadata includes isAccepted, follow link
  + title.
- EmailNotificationChannelTest: channelId, supports matrix, in-flight
  rejection, missing-email BusinessException, send routes to
  EmailService.sendEmail with templateId.
- WebSocketNotificationChannelTest: channelId, achievement emits
  BadgeEarnedPayload (badgeTier slug), submission emits
  NotificationPayload with isAccepted, contest reminder type tag.

EmailNotificationChannel.userMapper: package-private (was private) so
unit tests in the same package can inject a mock directly. Production
wiring is still via @Autowired(required=false).

mvn test on the 5 new test classes + the 4 existing legacy tests:
- 5 new classes: 28 tests, 0 failures
- 4 existing tests: 30 tests, 0 failures (legacy flag-off path verified)

Pre-existing ContestPublicControllerTest failure (ApplicationContext load
issue, unrelated to ADR-004) confirmed to fail on main without my changes.

Refs: docs/adr/ADR-004-notification-intents.md §4.
DavidHLP added a commit that referenced this pull request Jul 3, 2026
…t-skip

3 review findings addressed in 1 commit because they are all 1-line
behavior fixes (no record schema change, no test infrastructure change):

- EmailTemplates.forIntent: 5 null guards added to Map.of(...) calls
  (achievementName/contestTitle/replierUsername/title). Map.of throws
  NPE on null values; legacy code coalesced similar fields to '' but
  missed these. Fixes finding #1.
- WebSocketNotificationChannel.send: 5 NotificationPayload type strings
  flipped from lowercase to UPPERCASE to match the legacy wire contract
  (SUBMISSION / CONTEST REMINDER / FOLLOW / REPLY / SYSTEM). Frontend
  branches on payload.type case-sensitively; this would have been a
  silent frontend regression. Fixes finding #2.
- EmailNotificationChannel.send: missing-email path now silently returns
  (log.debug) instead of throwing BusinessException. ADR-004 §2.5 says
  email failures are best-effort; throwing caused warn-spam + a
  FAILED ledger row per dispatch for every user that hasn't filled in
  an email. Fixes finding #3.

Tests:
- WebSocketNotificationChannelTest updated to expect UPPERCASE type
  strings.
- EmailNotificationChannelTest#sendSilentlySkipsWhenUserHasNoEmail
  replaces the old #sendThrowsBusinessExceptionWhenUserHasNoEmail.

mvn test on the notification/follow/submission/email slice: green.

Refs: docs/adr/ADR-004-notification-intents.md (M4d-1 review findings
#1, #2, #3).
DavidHLP added a commit that referenced this pull request Jul 3, 2026
新建 docs/adr/ADR-005-rollback-drill-protocol.md, 是 ADR-005 §2.6 与
§4 #2 的执行规范. 包含:
- 11 步 drill 协议 (前置 / 执行 / 复盘 / 记录) 以 M3a 为例
- 4 个 milestone 的 Rollback 动作映射表
- 异常处理 3 条 (启动失败 / 业务异常 / 耗时超 50%)

dev 拓扑当 staging 演练, 不要求新建 staging 环境.

Refs: docs/adr/ADR-005-rolling-deploy-playbook.md §2.6, §4 #2
DavidHLP added a commit that referenced this pull request Jul 3, 2026
…rationProperties)

Pre-P0-2: FeatureFlagsProperties 用 flat 字段 judgeQueueUsePort/judgeQueueEnvelopeVersion,
但 application.yml 用 nested key app.features.judge-queue.use-port。Spring Binder 的
kebab→camel 转换只作用于叶节点名,不作用于路径段,导致 YAML override 永远不生效,
M3c cutover flag 静默卡在 false。

Fix: 改为 @NestedConfigurationProperty JudgeQueue inner class (usePort/envelopeVersion/cutoverAt),
与 app.features 整体 prefix 一致,方便后续 FlagCombinationValidator 单 bean 聚合校验。
cutover-at (F13 watermark) 之前被 Binder 静默忽略,本轮一并补字段。

调用方更新 (2 处):
- JudgeWorkerProcessor.pollAndProcessFromPort
- SubmissionServiceImpl.submit (portActive)

Note: JudgeOutboxDispatcher 仍用 @value 直接读 YAML key (reviewer 补充 #1),
task #7 统一切到 FeatureFlagsProperties,避免 YAML 双解析。
@ConditionalOnProperty 4 处 (RedissonStreamsJudgeQueueAdapter/QueueConfig/...) 不动
(reviewer 补充 #2 — 它们直接读 YAML key,P0-2 只修 API 暴露层)。

Tests: FeatureFlagsPropertiesBindingTest (3 case, 真实 Binder 端到端验证) +
回归 41 case 全 PASS (JudgeWorkerProcessorTest + P0-1 测试集)。

Refs: task #5 (P0-2)

Co-Authored-By: Claude <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jul 3, 2026
…formanceStats deep modules

Address the 2026-07-02 architecture review (candidates #1 and #2) by
deepening SubmissionServiceImpl two ways.

1. SubmissionPerformanceStats — moves ~130 lines of pure percentile /
   distribution-bin math (computePerformanceStats, buildDistributionBins,
   calculateBetterThanPercentile, formatDistributionBin) out of the
   1008-line service into a deep module (interface + DefaultSubmissionPerformanceStats),
   mirroring the SubmissionProjection / ProblemProjection pattern. Pure
   computation, zero I/O — now unit-testable without persistence mocks.

2. ContestSubmissionPort — inverts the submission -> contest dependency.
   SubmissionServiceImpl no longer imports any com.ulticode.modules.contest.*
   type: the four contest mappers and RealtimeService move behind a port
   owned by submission (recordSubmissionIfNeeded, isVirtualParticipation)
   and implemented by ContestSubmissionAdapter in the contest module (new
   contest/integration/ package). Behaviour is byte-for-byte identical (same
   RUNNING/STARTED gating, R6.2/F-06 clock selection, first-match-break,
   fire-and-forget semantics). Post-commit scoring stays event-driven via
   SubmissionJudgedEvent; only the synchronous same-transaction recording
   path was coupled (see ADR-0001).

Net: SubmissionServiceImpl 1008 -> ~830 lines, 17 -> 14 constructor deps,
submission module zero contest imports, submission unit tests mock 1 port
instead of 4 contest mappers. SubmissionServiceImplTest/IT updated to the
new constructor signature (stats wired real with the mocked SubmissionMapper
so the existing percentile/bin assertions exercise the extracted math
end-to-end).

Verification: ./mvnw compile green; ./mvnw test green for all submission
tests (SubmissionServiceImplTest passes). 9 pre-existing failures elsewhere
(JavaLanguageProfileTest, AdminSolutionServiceImplTest,
AdminProblemListControllerTest) are unrelated to this change — confirmed
identical on a clean tree with and without these changes (1207 tests, 9
failures, same set).

ADR-0001 records the decoupling decision and the rejected event-driven
alternative (conflicts with D-04 same-transaction invariant). CONTEXT.md
adds the domain glossary the architecture-review skill expects.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
…es to constants

Architecture review candidates #2 and #4:

- Delete useRetry.ts (266 LOC) and its test (213 LOC) — HTTP retry
  logic already lives in request.ts at the seam; the composable
  reimplemented calculateDelay/sleep/shouldRetryError on top.
- Move static code template data from useCodeTemplates composable
  (527 LOC) to constants/codeTemplates.ts (508 LOC). The composable
  wrapped immutable data in pointless computed() calls; the constants
  module exports plain functions. Fixes broken call site in
  CodeTemplatesModal.vue where the import was updated but the
  composable call was not.

Verified: 355 console tests pass, type-check clean.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
#1 SecurityUtil → CurrentUserProvider port
  - New CurrentUserProvider interface in common/auth/
  - SecurityCurrentUserProvider @component adapter (wraps SecurityContextHolder)
  - 37 files migrated from static SecurityUtil calls to injected interface
  - Eliminates duplicated userId==null guards across controller layer

#2 DashboardMapper+Service → DashboardStatsProjection
  - New DashboardStatsProjection interface + DefaultDashboardStatsProjection
  - Absorbs 7 private sub-aggregators + 6 mapper default methods
  - DashboardService + DashboardServiceImpl deleted
  - DashboardController now injects the projection directly
  - Mirrors ADR-0011 *Projection pattern

#3 AdminCommentService → CommentModerator polymorphic seam
  - New CommentModerator interface with ForumCommentModerator + SolutionCommentModerator
  - AdminCommentServiceImpl refactored to thin router
  - Eliminates 5x duplicated forum/solution type-string switch

#5 SubmissionMapper.calculateStreak → SubmissionStreakCalculator
  - New SubmissionStreakCalculator interface + JdbcSubmissionStreakCalculator
  - Streak algorithm (recursive CTE) now behind a JVM-testable interface
  - 3 production callers redirected + 3 test classes updated

#7 Frontend wrapper cleanup
  - 8 re-export wrapper files deleted (markdown, useTheme, useTypographyDensity, cn)
  - tsconfig path remaps @/lib/utils → @/shared/auth-core/src/utils
  - shared/sidebar-menu cn() consolidated to import from auth-core
  - LOCALE_HEADER_KEY duplicate declarations removed

#8 OAuthService → OAuthClient port + adapters
  - New OAuthClient interface + OAuthTokenResponse/OAuthUserInfo DTOs
  - GithubOAuthClient + GoogleOAuthClient @component adapters
  - OAuthService refactored to thin coordinator (221→195 LoC)
  - 11 unit tests covering dispatch, callback, DB upsert

Production code compiles (BUILD SUCCESS). 53 test compilation errors
from constructor signature changes — mechanical @mock field additions.
Candidates #4 (analytics projections) and #6 (contest.ts split) pending.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
Arch review #2. console/public and management/public theme-bootstrap.js were
byte-identical copies kept in sync by hand, plus a verify script that only
checked the two copies matched each other — nothing tied them to a canonical
source, so the seam could not prevent drift.

Promote shared/theme to the single source of truth:
- shared/theme/bootstrap.js — canonical FOUC bootstrap (plain IIFE JS, served
  as <script src> in <head> before the bundle; mirrors applyThemeToDOM.ts
  isDarkMode + reads THEME_STORAGE_KEY = 'ulticode-theme'). No TS->JS compile
  step introduced (that would reshape the build); the source is plain JS.
- scripts/sync-theme-bootstrap.mjs — regenerates both public copies from the
  source (`pnpm sync:theme-bootstrap`).
- scripts/verify-theme-sync.mjs — upgraded: now asserts each copy is
  byte-identical to the SOURCE (not just to each other) + behavior markers, so
  an edit to a copy, or to the source without a re-sync, fails CI.
- console/management package.json: add `sync:theme-bootstrap` entry.

The generated copies' IIFE is byte-identical to the previous logic, so FOUC
behavior is unchanged; only the leading comment + the generation contract
change. No CSP nonce added (index.html has no CSP today); the single-source
contract is ready for a future CSP that adds a nonce/hash to the <script src>.

Verified: verify-theme-sync OK (6 markers + copies identical to source);
console vue-tsc type-check clean.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
…ale-composable

Arch review 2026-07-10 candidate #2: locale-composable exported
`<L extends SupportedLocale, C>` where its own
`SupportedLocale = string` was a meaningless alias that
shadowed @ulticode/i18n-storage's tighter
`SupportedLocale = 'en-US' | 'zh-CN'`. Two packages, two
SupportedLocale types — a real type contract conflict.

Removed the redundant export. Generic bounds are now
`L extends string` directly (type-identical, since the previous
bound resolved to string anyway). The canonical union lives in
@ulticode/i18n-storage; each app re-exports it from its @/i18n
module.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
…arch-review #2)

Move the static ForumPostVOAssembler into a Spring-injected
ForumPostProjection / DefaultForumPostProjection pair. Both
read and write callers now depend on the same small interface
(3-arg or 5-arg toPostVO) instead of threading 4–6 collaborators
through static methods.

- New ForumPostProjection interface owns the batch (5-arg) and
  single-item (3-arg) entry points; the deletion test passes —
  removing the projection recreates the same parameter lists
  in both callers.
- DefaultForumPostProjection is the only place that touches
  ObjectMapper / voteService / mappers; helpers are private to
  the projection (parseTagList, parseMedia, attachVoteAndStats,
  attachCommunity, attachAuthor, attachMembership).
- DefaultForumReadProjection drops the static import and the
  unused mappers; injects the new projection.
- ForumPostServiceImpl drops the assembler import and the
  unused fields, keeps the create / update mutations.
- ForumPostVOAssembler deleted.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
…rt (arch-review #2)

Four submission paths (DefaultSubmissionWritePort existence check,
DefaultSubmissionProjection id/title/slug, CodeExecutionService limits
+ starter code, JudgedNotificationDispatcher title) each reached into
problem.mapper.* directly. Introduce a consumer-owned read seam:

- submission/port/ProblemFactsPort: findDisplayFacts / findLimits /
  findStarterCode, non-throwing (null on miss/error), mirroring the
  ContestSubmissionPort hexagonal convention.
- problem/port/ProblemFactsAdapter: production adapter owning all
  ProblemMapper / ProblemLanguageMapper reads.

Submission now imports zero problem.mapper types. ADR-0011 write-side
ProblemDetailPort is untouched. Behaviour preserved: debounce/search
unchanged; safe-degrade fallbacks retained via the non-throwing port.

Validated: mvn compile + test-compile 0 errors; DefaultSubmissionProjection/
CodeExecution/SubmissionServiceImpl unit tests green.

Co-Authored-By: Claude <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jul 16, 2026
Implements candidates from /tmp/architecture-review-1783827842.html:

#1 AdminBulkExecutor (admin/bulk): lifts the duplicated per-item try/catch
loop + counting + optional existence-guard out of AdminForum/Comment/
Solution/Problem bulk actions into one consumer-owned executor with a
canonical ItemOutcome/Run. batchRejudge withdrawn with evidence (rich
RejudgeResult shape ≠ void-action aggregated contract).

#2 ProblemExportService + ExportPayload: moves format validation, the 10k
size cap, CSV header/escaping, and the LocalDate.now() time-leak out of
AdminProblemController into a Clock-injected service; controller shrinks
to response-writing.

#3 LegacyRejudgeStrategy + RejudgePolicy.rejudge dispatcher: moves the
50-line inline legacy rejudge state machine out of AdminSubmissionServiceImpl
behind the policy port; fenced vs legacy selection now lives in
DefaultRejudgePolicy. AdminSubmissionServiceImpl.rejudge = 3-line dispatch.

#4 createAuthStore factory (shared/auth-core): owns the duplicated
login/logout/fetchUser/loadPermissions/initialize/clearUser/hasPermission
chain + CSRF contract once; management store migrated. Console deferred
(already composable-shaped with a richer status state machine).

#6 AdminContestReadPort + adapter: ADR-0011 phase 2 (contest) — admin
contest reads cross the seam instead of reaching into ContestProblemMapper.

#5 (AdminCrudListView wrapper) withdrawn: scaffolded then deleted after
code-review flagged it as dead code (0 views wired); the genuine shared
seam already lives in DataTable + useDataTable + DataTableToolbar.

Validation: mvn compile + test-compile green; 37 targeted admin/submission
tests pass (incl. new AdminBulkExecutorTest); management eslint green on
auth store. code-review skill run (Standards pass, Spec gaps documented
in-code).

Co-Authored-By: Claude <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jul 20, 2026
Closes the Standards-axis judgement calls the two-axis review raised
(0 hard violations; these are the value-bearing ones):

- C06 data clump: ModerationServiceImpl.applyAction took 8 positional
  params (action, moderatorId, note, durationDays, now, queueId,
  actionId, item). Introduced a narrow ActionRequest value record for
  the per-action inputs so the switch reads (request, item). The record
  is a pure value, not the old ActionContext service-callback proxy.
- C01 primitive obsession + edge case: buildScoredContestProblems used a
  bare literal 100 for the default score and a bare (char)('A'+i) index
  that silently overflowed to non-letters past 26 problems. Named the
  default DEFAULT_PROBLEM_SCORE and extracted problemIndex(i) which
  returns A-Z for slots 0-25 and a deterministic P<n> label beyond,
  instead of garbage chars.
- C02 magic numbers: useProblemRun carried bare "javascript", 60, 5000.
  Named DEFAULT_RUN_LANGUAGE, RATE_LIMIT_FALLBACK_WAIT_SECONDS,
  RATE_LIMIT_TOAST_DURATION_MS.

Behaviour preserved. #2 (parseRuntimeMs home) and #6 (getSocketManager
name) left: parseRuntimeMs's docstring already distinguishes the sandbox
wire format from the queue parser, and renaming getSocketManager would
churn four callers for a cosmetic gain.

Validated: ModerationServiceImplTest(11) + AdminContestMutationServiceImplTest
green; console type-check + eslint clean; problem-detail suite(12) green.

Co-Authored-By: Claude <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jul 20, 2026
…nService

Architecture-review 2026-07-19 candidate #2 noted the class-level
self-description ('Thin facade for code execution (M2a, ADR-002)')
no longer matched the implementation: the class owns intake validation,
signature-cache + DTO enrichment, per-problem resource-limit resolution,
sandbox translation, verdict reduction, and per-case / batch dispatch.

- Rewrites the javadoc to describe Problem run module ownership honestly.
- Drops the dangling ADR-002 / M2a / F15-F16 / SandboxService citations
  that point to documents/identifiers that do not exist in the repo.
- References only symbols that resolve in-source ({@link SandboxExecutor},
  {@link JudgingLanguageSupport}, {@link ProblemFactsPort}, etc.).

No behavior change; javadoc only.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant