Skip to content

feat: UltiCode Platform Enhancement - CI/CD, PWA, Payment & Analytics Redesign - #4

Merged
DavidHLP merged 3 commits into
mainfrom
feat/ci-cd-workflow
Mar 4, 2026
Merged

feat: UltiCode Platform Enhancement - CI/CD, PWA, Payment & Analytics Redesign#4
DavidHLP merged 3 commits into
mainfrom
feat/ci-cd-workflow

Conversation

@DavidHLP

@DavidHLP DavidHLP commented Mar 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • Analytics Dashboard Redesign: Complete UI overhaul with precision dashboard design

    • New sidebar navigation replacing top tabs
    • 5 new reusable analytics components
    • Bento grid layout for all 5 report types
    • Silver-gray palette with ice-blue accents
  • PWA Offline Support: Full offline functionality for console app

    • Service worker with workbox
    • Offline submission queue
    • PWA composables and utilities
  • Payment Integration: Complete Stripe implementation

    • Subscription management
    • Webhook handling
    • Invoice generation
  • OAuth Enhancement: GitHub + Google login support

  • CI/CD Workflow: Build, lint, type-check automation

New Components

Component Purpose
AnalyticsNav Sidebar navigation with active states
AnalyticsMetricCard Metric cards with trend indicators
AnalyticsBarList Horizontal bar ranking lists
AnalyticsTagCloud Size-mapped tag clouds
AnalyticsHeatmap 24-hour activity heatmap

Test plan

  • Verify sidebar navigation switches between report types
  • Check all 5 analytics reports render correctly
  • Verify responsive layout on mobile/tablet/desktop
  • Test dark mode styling
  • Confirm i18n translations work (EN/CN)
  • Verify PWA offline functionality
  • Test Stripe payment flow
  • Verify OAuth login (GitHub/Google)

- Replace top tabs with sidebar navigation for better UX
- Create 5 new analytics components:
  - AnalyticsNav: Side navigation with icons and active states
  - AnalyticsMetricCard: Metric cards with trend indicators
  - AnalyticsBarList: Horizontal bar chart ranking lists
  - AnalyticsTagCloud: Tag cloud with size-mapped values
  - AnalyticsHeatmap: 24-hour activity heatmap
- Refactor AnalyticsView layout:
  - Sidebar navigation + content area layout
  - Bento grid layout for each report type
  - Consistent precision card styling
- Update dashboard components with precision design:
  - AreaChart, StatCards, DashboardTimeline
- Add i18n translations for new labels
- Remove completed BUSINESS_IMPROVEMENT_PLAN.md

Design system: silver-gray palette, ice-blue accent, monospace
data fonts, subtle hover effects

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4672f26e00

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread management/src/components/analytics/AnalyticsHeatmap.vue
Comment thread management/src/views/analytics/AnalyticsView.vue
DavidHLP added 2 commits March 4, 2026 22:40
Comprehensive refactoring of management frontend with terminal-style design:

- Add new terminal UI components (TerminalCard, TerminalBadge, TerminalInput, DataBlock)
- Create reusable auth components (AuthCard, AuthButton, AuthInput, AuthDivider, OAuthButton)
- Migrate all views to terminal precision design:
  - Audit module (LogsView, ReportView, DetailDrawer)
  - Auth views (Login, Signup with modern card layouts)
  - Comments, Contests, Forum, Moderation, Notifications
  - Problem lists, Problems, Solutions, Submissions, Users
- Extract table columns into dedicated files for maintainability
- Extend i18n with new terminal-themed translations
- Update core UI components (Input, Select, Checkbox, Avatar, DropdownMenu)
- Add 392 lines of terminal precision design CSS utilities
Remove manual `actions/cache@v4` steps that use wildcard paths
(`**/node_modules`), which cause tar extraction failures in cache restore.

The `actions/setup-node@v4` already has `cache: 'pnpm'` enabled, which
automatically caches the pnpm store (~/.local/share/pnpm/store). Since
pnpm uses hard links from the store, installation remains fast without
manual node_modules caching.
@DavidHLP
DavidHLP merged commit 976e095 into main Mar 4, 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
…_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
…00 on bad JSON + mask SMTP password

Closes all 5 P0/P1 bugs from docs/SETTINGS_API_TEST_REPORT_2026-06-09.md
plus 3 review findings (M1/L2/L3/L4) from .claude/reviews/settings-fix-review.md.

Bugs fixed
- §5.1 PATCH/POST endpoints were no-op stubs (echoed input, never wrote
  to DB). All 14 endpoints now persist via SystemSettingsService backed by
  the existing `system_settings` table (one JSON-serialized row per
  category: general/email/rate-limits/uploads/features).
- §5.2 Maintenance toggle set `maintenanceMode` on MaintenanceModeVO only
  — GET /admin/settings still returned false. Now toggles the same row
  read by the general endpoint, so a single source of truth.
- §5.3 Malformed JSON / type mismatches returned 500 'Unknown error'.
  Added handlers for HttpMessageNotReadableException and
  MethodArgumentTypeMismatchException in GlobalExceptionHandler; both
  return 400 with descriptive messages.
- §5.4 Empty body `{}` silently overwrote all fields with defaults.
  MaintenanceModeRequest.enabled is now @NotNull Boolean (boxed) so
  missing field is rejected; PATCH features with all 8 flags at JSON
  default (false) is now rejected by the service as an accidental
  empty-PATCH safety guard (M1).
- §8.1 SMTP password returned in cleartext. PASSWORD_MASK = '***' is
  returned on GET; PATCH with mask preserves existing password;
  PATCH with new value overwrites.

Review findings applied
- M1: service rejects 'PATCH features with all 8 flags false' as
  BusinessException(SETTING_INVALID_VALUE, 200002) — empty body
  detection. Frontend should always send all 8 flags; to truly take
  the platform offline, use POST /admin/settings/maintenance.
- L2: removed unreachable null check in toggleMaintenance —
  @NotNull on Boolean + Spring's body binding already reject null
  with 400 via the global handler.
- L3: resetToDefaults and toggleMaintenance now log an 'AUDIT'
  line with the Spring Security principal from SecurityContextHolder,
  so destructive operations are traceable.
- L4: getAllSettings now uses one selectBatchIds query (5 keys)
  instead of 5 sequential selectById round-trips.

Security: `key` is a MySQL reserved word; entity uses @TableId("`key`")
to force backtick quoting. Verified via Testcontainers IT.

BREAKING CHANGE
- PATCH /admin/settings body shape: was `AllSettingsVO` (28 fields),
  now `GeneralSettingsVO` (6 fields). The parent path now manages the
  general category. If a client used the 28-field payload, Jackson
  silently ignores unknown fields — the persisted state is the
  intersection. To patch all 28 fields, use PATCH on each of
  /admin/settings/{email,rate-limits,uploads,features} and PATCH /admin/settings
  for general. Documented in docs/SETTINGS_API_FIX_REPORT_2026-06-09.md
  §6 #4.
- PATCH /admin/settings/features rejects all-defaults (see M1).
  Clients that need 'disable every feature' must use maintenance mode
  instead.

Tests
- AdminSettingsControllerTest: 20 cases (14 happy + 4 negative + 2 routing),
  all green.
- SystemSettingsServiceImplIT: 11 cases (Testcontainers MySQL), including
  P0 regressions for §5.1, §5.2, §8.1, plus the new M1 'all defaults
  rejected' and 'one flag true accepted' assertions.

Files
- +12 new: 8 DTOs, SystemSetting entity, mapper, service interface,
  service impl, controller test, IT.
- ~3 modified: AdminSettingsController (387 -> 150 lines), ErrorCode
  (+4 SETTING_* codes), GlobalExceptionHandler (+2 400 handlers).
- +2 docs: original test report and the regression report.

Reported-by: docs/SETTINGS_API_TEST_REPORT_2026-06-09.md
Reviewed-by: .claude/reviews/settings-fix-review.md (APPROVE with comments)
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 10, 2026
- UserStatsDTO$DifficultyStats.getCount() NoSuchMethodError
  on /users/{id}/stats, /users/{id}/profile, /users/by-username/{u}/profile
- /v3/api-docs 500 due to missing RunResultDTO$RunResultDTOBuilder

Root cause: target/classes/ had stale Lombok-failed class files.
PM2 uses spring-boot:run which reads target/classes/ directly, not
the packaged app.jar. The mismatch was introduced when service code
(UserServiceImpl.getUserStatsById) was updated to call getCount()
but the DTO recompile skipped the inner class regeneration.

Fix: mvnw clean install -DskipTests + pm2 restart ulticode-9001.
No source changes. Verified via javap -p that Lombok methods are
regenerated (getCount/getTotal/setCount/setTotal/equals/hashCode/toString
on DifficultyStats; RunResultDTOBuilder inner class on RunResultDTO).

All 4 originally-500 endpoints now return 200:
- GET /users/{id}/stats
- GET /users/{id}/profile
- GET /users/by-username/{u}/profile
- GET /v3/api-docs

Verified via curl + the report docs/api-test-report-user-userstats.md.

Refs: docs/api-test-report-user-userstats.md (Defect #1 + #4)
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
…-claim 漏洞)

Review finding #4: tryClaim uses ON DUPLICATE KEY UPDATE id=id, so
any existing row (including a stuck CLAIMED from a crashed dispatcher
or channel JVM) returns 0 on retry → dispatcher treats 0 as 'already
delivered, skip' → user permanently misses that channel's delivery.
No reaper was in the M4a-d implementation; the DeliveryState javadoc
explicitly deferred this to 'a future reaper'.

New NotificationLedgerReaper:
- @scheduled(fixedDelay=5min, initialDelay=1min) — interval is
  overridable via ulticode.notification.ledger.reaper-interval-ms.
- reapStaleClaimed() transitions CLAIMED rows older than 10 minutes
  to FAILED with a clear failure_reason. The 10-minute grace covers
  slow SMTP responses (Email is the slowest channel); stuck rows are
  usually fixed within 1-2 reaper cycles.
- Counter notification.ledger.reaper.reaped exposes the count for ops
  dashboards. A non-zero value is a real signal of dispatcher/channel
  JVM crashes and warrants investigation.
- Reaper exceptions are swallowed (logged at warn) so a single DB
  hiccup does not stop the scheduled task from firing every 5 minutes.

This is a minimal stop-gap; the durable retry path (ADR-007) is the
long-term answer. The reaper only transitions the row to FAILED — a
future outbox-style retry can still re-attempt the original intent
because tryClaim is keyed on (intent_id, channel_id) and the existing
row's state is informational.

mvn compile: green. mvn test on the notification slice: green (new
mapper method not yet covered by a unit test — added in a follow-up
chore alongside the reaper metrics tests).

Refs: docs/adr/ADR-004-notification-intents.md (M4d-1 review finding
#4); DeliveryState javadoc CLAIMED → DELIVERED/FAILED transition.
DavidHLP pushed a commit that referenced this pull request Jun 13, 2026
ADR-005 §4 #4: 5 产品 flag + 5 cutover flag 矩阵化, 用于 CI matrix.
- application-features-off.yml: 全部 10 flag 显式 false/默认值, 与
  FeatureFlagsProperties 默认值一致
- application-features-on.yml: 5 产品 flag 全部 true, cutover flag 保持
  (走 cutover 守门, 不在 CI matrix 强开)

配合 -Dspring.profiles.active=ci,features-{off,on} 使用.
T4 会加 ci.yml matrix 把默认 backend-test job 复制为 off/on 两变体.

Refs: docs/adr/ADR-005-rolling-deploy-playbook.md §4 Row #4
DavidHLP pushed a commit that referenced this pull request Jun 13, 2026
…atrix)

ADR-005 §4 #4: ci.yml add backend-test-features-off and
backend-test-features-on jobs, same shape as backend-test (mysql/redis
services + JDK 17 + mvnw test), but:
- -Dspring.profiles.active appends features-{off,on}
- env block explicitly overrides 10 env vars (USE_JUDGE_OUTBOX etc.) as
  belt-and-suspenders against profile-not-loaded silent fallback
- features-on flips 5 product flags true; cutover flags stay default

3 jobs (default / off / on) let PRs run all 3 paths, catching behavior
drift between "default-flag path" and "all-off / all-on paths".

Refs: docs/adr/ADR-005-rolling-deploy-playbook.md Row #4
DavidHLP pushed a commit that referenced this pull request Jun 13, 2026
Code review finding R1 (HIGH): backend-test-features-off 与 -on 2 job
复制粘贴 162 行 services+env+steps, 改 backend-test 模板 (JDK 升 / mysql
镜像升 / 加 env) 容易漏改. ci.yml 已有 4 处 matrix 用法 (frontend lint/
type-check/test + docker-build), 现成模式.

修复: 合并为 1 个 backend-test-features job + matrix.features=[off,on],
服务 + 5 cutover flag env 不变, 5 产品 flag 由 step 内部根据 matrix
注入 GITHUB_ENV. 改 backend-test 模板 (JDK 17 / mysql 9.1 / redis 7
镜像升级) 只需同步改 backend-test 与 backend-test-features 2 job,
不再 3 job.

注: 原 backend-test (default profile) 保留, 不走 matrix, 避免动
PR status check 名称 (项目 owner dashboard 已记录该 job 名).

Refs: docs/adr/ADR-005-rolling-deploy-playbook.md §4 Row #4
DavidHLP added a commit that referenced this pull request Jun 13, 2026
…-claim 漏洞)

Review finding #4: tryClaim uses ON DUPLICATE KEY UPDATE id=id, so
any existing row (including a stuck CLAIMED from a crashed dispatcher
or channel JVM) returns 0 on retry → dispatcher treats 0 as 'already
delivered, skip' → user permanently misses that channel's delivery.
No reaper was in the M4a-d implementation; the DeliveryState javadoc
explicitly deferred this to 'a future reaper'.

New NotificationLedgerReaper:
- @scheduled(fixedDelay=5min, initialDelay=1min) — interval is
  overridable via ulticode.notification.ledger.reaper-interval-ms.
- reapStaleClaimed() transitions CLAIMED rows older than 10 minutes
  to FAILED with a clear failure_reason. The 10-minute grace covers
  slow SMTP responses (Email is the slowest channel); stuck rows are
  usually fixed within 1-2 reaper cycles.
- Counter notification.ledger.reaper.reaped exposes the count for ops
  dashboards. A non-zero value is a real signal of dispatcher/channel
  JVM crashes and warrants investigation.
- Reaper exceptions are swallowed (logged at warn) so a single DB
  hiccup does not stop the scheduled task from firing every 5 minutes.

This is a minimal stop-gap; the durable retry path (ADR-007) is the
long-term answer. The reaper only transitions the row to FAILED — a
future outbox-style retry can still re-attempt the original intent
because tryClaim is keyed on (intent_id, channel_id) and the existing
row's state is informational.

mvn compile: green. mvn test on the notification slice: green (new
mapper method not yet covered by a unit test — added in a follow-up
chore alongside the reaper metrics tests).

Refs: docs/adr/ADR-004-notification-intents.md (M4d-1 review finding
#4); DeliveryState javadoc CLAIMED → DELIVERED/FAILED transition.
DavidHLP added a commit that referenced this pull request Jun 13, 2026
ADR-005 §4 #4: 5 产品 flag + 5 cutover flag 矩阵化, 用于 CI matrix.
- application-features-off.yml: 全部 10 flag 显式 false/默认值, 与
  FeatureFlagsProperties 默认值一致
- application-features-on.yml: 5 产品 flag 全部 true, cutover flag 保持
  (走 cutover 守门, 不在 CI matrix 强开)

配合 -Dspring.profiles.active=ci,features-{off,on} 使用.
T4 会加 ci.yml matrix 把默认 backend-test job 复制为 off/on 两变体.

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

ADR-005 §4 #4: ci.yml add backend-test-features-off and
backend-test-features-on jobs, same shape as backend-test (mysql/redis
services + JDK 17 + mvnw test), but:
- -Dspring.profiles.active appends features-{off,on}
- env block explicitly overrides 10 env vars (USE_JUDGE_OUTBOX etc.) as
  belt-and-suspenders against profile-not-loaded silent fallback
- features-on flips 5 product flags true; cutover flags stay default

3 jobs (default / off / on) let PRs run all 3 paths, catching behavior
drift between "default-flag path" and "all-off / all-on paths".

Refs: docs/adr/ADR-005-rolling-deploy-playbook.md Row #4
DavidHLP added a commit that referenced this pull request Jun 13, 2026
Code review finding R1 (HIGH): backend-test-features-off 与 -on 2 job
复制粘贴 162 行 services+env+steps, 改 backend-test 模板 (JDK 升 / mysql
镜像升 / 加 env) 容易漏改. ci.yml 已有 4 处 matrix 用法 (frontend lint/
type-check/test + docker-build), 现成模式.

修复: 合并为 1 个 backend-test-features job + matrix.features=[off,on],
服务 + 5 cutover flag env 不变, 5 产品 flag 由 step 内部根据 matrix
注入 GITHUB_ENV. 改 backend-test 模板 (JDK 17 / mysql 9.1 / redis 7
镜像升级) 只需同步改 backend-test 与 backend-test-features 2 job,
不再 3 job.

注: 原 backend-test (default profile) 保留, 不走 matrix, 避免动
PR status check 名称 (项目 owner dashboard 已记录该 job 名).

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

Add LOCALE_VARIANT_MAP for explicit handling of common locale variants:
- Chinese Simplified: zh, zh-Hans, zh-Hans-CN, zh-Hans-SG, zh-SG → zh-CN
- English variants: en, en-GB, en-AU, en-CA, en-IN, en-NZ, en-ZA, en-IE → en-US

Rewrite matchSupportedLocale with three-tier approach:
1. Exact match against SUPPORTED_LOCALES
2. Explicit variant mapping lookup
3. Base language code fallback for unlisted variants

Add comprehensive unit tests (33 tests) covering:
- Exact matches, variant mappings, language code fallback
- Unsupported locales (ja-JP, fr-FR, etc.)
- Edge cases (empty string, whitespace, case variations)

The implementation preserves existing behavior while providing:
- Clear documentation for future locale additions (e.g., zh-TW)
- Better maintainability through explicit mappings
- Case-permissive matching for user convenience

Fixes issue #4 from temporal-herding-hamster.md
DavidHLP added a commit that referenced this pull request Jul 3, 2026
feat: UltiCode Platform Enhancement - CI/CD, PWA, Payment & Analytics Redesign
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
…_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
…00 on bad JSON + mask SMTP password

Closes all 5 P0/P1 bugs from docs/SETTINGS_API_TEST_REPORT_2026-06-09.md
plus 3 review findings (M1/L2/L3/L4) from .claude/reviews/settings-fix-review.md.

Bugs fixed
- §5.1 PATCH/POST endpoints were no-op stubs (echoed input, never wrote
  to DB). All 14 endpoints now persist via SystemSettingsService backed by
  the existing `system_settings` table (one JSON-serialized row per
  category: general/email/rate-limits/uploads/features).
- §5.2 Maintenance toggle set `maintenanceMode` on MaintenanceModeVO only
  — GET /admin/settings still returned false. Now toggles the same row
  read by the general endpoint, so a single source of truth.
- §5.3 Malformed JSON / type mismatches returned 500 'Unknown error'.
  Added handlers for HttpMessageNotReadableException and
  MethodArgumentTypeMismatchException in GlobalExceptionHandler; both
  return 400 with descriptive messages.
- §5.4 Empty body `{}` silently overwrote all fields with defaults.
  MaintenanceModeRequest.enabled is now @NotNull Boolean (boxed) so
  missing field is rejected; PATCH features with all 8 flags at JSON
  default (false) is now rejected by the service as an accidental
  empty-PATCH safety guard (M1).
- §8.1 SMTP password returned in cleartext. PASSWORD_MASK = '***' is
  returned on GET; PATCH with mask preserves existing password;
  PATCH with new value overwrites.

Review findings applied
- M1: service rejects 'PATCH features with all 8 flags false' as
  BusinessException(SETTING_INVALID_VALUE, 200002) — empty body
  detection. Frontend should always send all 8 flags; to truly take
  the platform offline, use POST /admin/settings/maintenance.
- L2: removed unreachable null check in toggleMaintenance —
  @NotNull on Boolean + Spring's body binding already reject null
  with 400 via the global handler.
- L3: resetToDefaults and toggleMaintenance now log an 'AUDIT'
  line with the Spring Security principal from SecurityContextHolder,
  so destructive operations are traceable.
- L4: getAllSettings now uses one selectBatchIds query (5 keys)
  instead of 5 sequential selectById round-trips.

Security: `key` is a MySQL reserved word; entity uses @TableId("`key`")
to force backtick quoting. Verified via Testcontainers IT.

BREAKING CHANGE
- PATCH /admin/settings body shape: was `AllSettingsVO` (28 fields),
  now `GeneralSettingsVO` (6 fields). The parent path now manages the
  general category. If a client used the 28-field payload, Jackson
  silently ignores unknown fields — the persisted state is the
  intersection. To patch all 28 fields, use PATCH on each of
  /admin/settings/{email,rate-limits,uploads,features} and PATCH /admin/settings
  for general. Documented in docs/SETTINGS_API_FIX_REPORT_2026-06-09.md
  §6 #4.
- PATCH /admin/settings/features rejects all-defaults (see M1).
  Clients that need 'disable every feature' must use maintenance mode
  instead.

Tests
- AdminSettingsControllerTest: 20 cases (14 happy + 4 negative + 2 routing),
  all green.
- SystemSettingsServiceImplIT: 11 cases (Testcontainers MySQL), including
  P0 regressions for §5.1, §5.2, §8.1, plus the new M1 'all defaults
  rejected' and 'one flag true accepted' assertions.

Files
- +12 new: 8 DTOs, SystemSetting entity, mapper, service interface,
  service impl, controller test, IT.
- ~3 modified: AdminSettingsController (387 -> 150 lines), ErrorCode
  (+4 SETTING_* codes), GlobalExceptionHandler (+2 400 handlers).
- +2 docs: original test report and the regression report.

Reported-by: docs/SETTINGS_API_TEST_REPORT_2026-06-09.md
Reviewed-by: .claude/reviews/settings-fix-review.md (APPROVE with comments)
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
- UserStatsDTO$DifficultyStats.getCount() NoSuchMethodError
  on /users/{id}/stats, /users/{id}/profile, /users/by-username/{u}/profile
- /v3/api-docs 500 due to missing RunResultDTO$RunResultDTOBuilder

Root cause: target/classes/ had stale Lombok-failed class files.
PM2 uses spring-boot:run which reads target/classes/ directly, not
the packaged app.jar. The mismatch was introduced when service code
(UserServiceImpl.getUserStatsById) was updated to call getCount()
but the DTO recompile skipped the inner class regeneration.

Fix: mvnw clean install -DskipTests + pm2 restart ulticode-9001.
No source changes. Verified via javap -p that Lombok methods are
regenerated (getCount/getTotal/setCount/setTotal/equals/hashCode/toString
on DifficultyStats; RunResultDTOBuilder inner class on RunResultDTO).

All 4 originally-500 endpoints now return 200:
- GET /users/{id}/stats
- GET /users/{id}/profile
- GET /users/by-username/{u}/profile
- GET /v3/api-docs

Verified via curl + the report docs/api-test-report-user-userstats.md.

Refs: docs/api-test-report-user-userstats.md (Defect #1 + #4)
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
…-claim 漏洞)

Review finding #4: tryClaim uses ON DUPLICATE KEY UPDATE id=id, so
any existing row (including a stuck CLAIMED from a crashed dispatcher
or channel JVM) returns 0 on retry → dispatcher treats 0 as 'already
delivered, skip' → user permanently misses that channel's delivery.
No reaper was in the M4a-d implementation; the DeliveryState javadoc
explicitly deferred this to 'a future reaper'.

New NotificationLedgerReaper:
- @scheduled(fixedDelay=5min, initialDelay=1min) — interval is
  overridable via ulticode.notification.ledger.reaper-interval-ms.
- reapStaleClaimed() transitions CLAIMED rows older than 10 minutes
  to FAILED with a clear failure_reason. The 10-minute grace covers
  slow SMTP responses (Email is the slowest channel); stuck rows are
  usually fixed within 1-2 reaper cycles.
- Counter notification.ledger.reaper.reaped exposes the count for ops
  dashboards. A non-zero value is a real signal of dispatcher/channel
  JVM crashes and warrants investigation.
- Reaper exceptions are swallowed (logged at warn) so a single DB
  hiccup does not stop the scheduled task from firing every 5 minutes.

This is a minimal stop-gap; the durable retry path (ADR-007) is the
long-term answer. The reaper only transitions the row to FAILED — a
future outbox-style retry can still re-attempt the original intent
because tryClaim is keyed on (intent_id, channel_id) and the existing
row's state is informational.

mvn compile: green. mvn test on the notification slice: green (new
mapper method not yet covered by a unit test — added in a follow-up
chore alongside the reaper metrics tests).

Refs: docs/adr/ADR-004-notification-intents.md (M4d-1 review finding
#4); DeliveryState javadoc CLAIMED → DELIVERED/FAILED transition.
DavidHLP added a commit that referenced this pull request Jul 3, 2026
ADR-005 §4 #4: 5 产品 flag + 5 cutover flag 矩阵化, 用于 CI matrix.
- application-features-off.yml: 全部 10 flag 显式 false/默认值, 与
  FeatureFlagsProperties 默认值一致
- application-features-on.yml: 5 产品 flag 全部 true, cutover flag 保持
  (走 cutover 守门, 不在 CI matrix 强开)

配合 -Dspring.profiles.active=ci,features-{off,on} 使用.
T4 会加 ci.yml matrix 把默认 backend-test job 复制为 off/on 两变体.

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

ADR-005 §4 #4: ci.yml add backend-test-features-off and
backend-test-features-on jobs, same shape as backend-test (mysql/redis
services + JDK 17 + mvnw test), but:
- -Dspring.profiles.active appends features-{off,on}
- env block explicitly overrides 10 env vars (USE_JUDGE_OUTBOX etc.) as
  belt-and-suspenders against profile-not-loaded silent fallback
- features-on flips 5 product flags true; cutover flags stay default

3 jobs (default / off / on) let PRs run all 3 paths, catching behavior
drift between "default-flag path" and "all-off / all-on paths".

Refs: docs/adr/ADR-005-rolling-deploy-playbook.md Row #4
DavidHLP added a commit that referenced this pull request Jul 3, 2026
Code review finding R1 (HIGH): backend-test-features-off 与 -on 2 job
复制粘贴 162 行 services+env+steps, 改 backend-test 模板 (JDK 升 / mysql
镜像升 / 加 env) 容易漏改. ci.yml 已有 4 处 matrix 用法 (frontend lint/
type-check/test + docker-build), 现成模式.

修复: 合并为 1 个 backend-test-features job + matrix.features=[off,on],
服务 + 5 cutover flag env 不变, 5 产品 flag 由 step 内部根据 matrix
注入 GITHUB_ENV. 改 backend-test 模板 (JDK 17 / mysql 9.1 / redis 7
镜像升级) 只需同步改 backend-test 与 backend-test-features 2 job,
不再 3 job.

注: 原 backend-test (default profile) 保留, 不走 matrix, 避免动
PR status check 名称 (项目 owner dashboard 已记录该 job 名).

Refs: docs/adr/ADR-005-rolling-deploy-playbook.md §4 Row #4
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
Single source of truth for the inline hasPermission / hasRole / hasAnyRole
logic that lived in both auth stores (arch review candidate #4). Supports
*:* / action:* / *:resource / exact-match wildcards for checkPermission;
case-insensitive comparison for role checks. Exports added in index.ts.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
…norm rule

Architecture review candidate #4 — three confused vote paths:

1. POST /vote (VoteController) → voteService.vote() — pure vote-state
   mutation, no side effects. Legitimate.

2. POST /edge-operations with VOTE_UP/DOWN (EdgeOperationsController) →
   voteService.vote() + denormalized counter projection onto solution.likes.
   Legitimate — owns the analytics + denorm side-effects.

3. ForumVoteServiceImpl (27 LoC) — type-alias pass-through that delegates
   to voteService.getVoteStatus with EdgeOperationTargetType.FORUM_POST
   hardcoded. ZERO callers (verified by grep across main/ and test/).
   Pure dead code.

This commit:
  - Deletes ForumVoteService.java (interface, 19 LoC)
  - Deletes ForumVoteServiceImpl.java (impl, 27 LoC)
  - Updates AGENTS.md vote rule to document:
      * Denormalization is the caller's responsibility (not VoteService's)
      * The two legitimate routes (VoteController, EdgeOperationsController)
      * The asymmetry (only EdgeOperations projects onto solution.likes) is
        deliberate — coupling VoteService to Solution would be worse than
        the asymmetry
      * Future VoteResultUpdated domain-event pattern would resolve the
        asymmetry without coupling
      * ForumVoteService deletion is permanent — do not resurrect

Architecture-review proposal to move updateSolutionVoteCounts INTO
VoteServiceImpl is REJECTED — that would couple Vote (generic, votes on
Solutions + Forum Posts + other types) to Solution (specific entity with
a denormalized column). The review was right that the topology was
confusing but wrong about the fix.

Verified: ./mvnw compile clean. No tests referenced ForumVoteService
(grep across src/test/ returned zero matches).
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
9 management views (ForumPostDetailDrawer, AuditTab, OverviewDisplay, BackupView,
EmailView, AuditLogViewer, ActionHistoryTimeline, ModerationDetailDrawer,
EntityPreviewCard) each defined a local `formatDate` that only forwarded to
`formatDateTimeByLocale` — pure Middle Man. Remove the wrappers; templates now
call `formatDateTimeByLocale` directly (already imported from @/i18n/utils).

Arch review #4 scope notes:
- management/lib/format/date.ts is RETAINED — it is not a duplicate of
  shared/datetime-utils but a locale default-injection seam (18 callers rely on
  its locale-less signature). Earlier CR suggestion to delete it was retracted
  after investigation.
- Inline formatters that remain are intentionally different output semantics,
  kept per the review's "preserve deliberately different business display":
  DashboardView.formatRelativeTime (i18n keys), BackupView/MonitoringView
  formatBytes/formatUptime, and console's locale-specific formatDate/formatTime
  variants (long-month, no-year, ISO, conditional today/time).

Verified: management vue-tsc type-check clean.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
… color maps

Arch review 2026-07-10 candidate #4: CONTEST_STATUS_COLOR_MAP
held both lowercase and uppercase variants of the same status
('draft'/'DRAFT', 'upcoming'/'UPCOMING', 'finished'/'FINISHED')
plus a set of management-only UI states ('published',
'registering', 'ongoing', 'freezing', 'archived') that do not
exist in the backend ContestStatus enum. Grep verified zero
callers pass lowercase keys. DIFFICULTY_COLOR_MAP had the same
issue with easy/medium/hard fallbacks.

Removed all lowercase aliasing; the maps now mirror only the
backend canonical values. Added normalizeContestStatusKey()
helper next to the map for future defensive callers. Management's
UI-only contest states stay owned by the local ContestStatusBadge
component (its own statusConfig), not the shared presentation
seam.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
Pull the lifecycle (claim → heartbeat → execute → verdict → contest
effect → push → release) out of JudgeWorkerProcessor into a
JudgeAttemptExecutor / DefaultJudgeAttemptExecutor pair:

- The worker becomes a thin polling adapter: poll loop, envelope
  reconstruction, and the public JobProcessor interface only. It
  no longer holds the lease CAS, the heartbeat executor, the
  push port, or the contest-submission mapper.
- The executor owns the legacy / fenced cutover (selected by the
  shared FeatureFlagsProperties.isUseGenerationFence() flag) and
  is the only place that calls SubmissionService,
  SubmissionResultPushPort, ContestSubmissionMapper, or runs the
  heartbeat scheduler. Tests can drive the full attempt against
  the leased or RQueue path without mocking the worker.
- The onFailure retry-exhausted path also routes through the
  executor (new markExhausted entry point) so the
  System-Error write + push lives in one place.
- enqueueId() → envelope.id() fix on the leased path.

net: 544 lines of JudgeWorkerProcessor collapse to 240; the
executor is 305 lines. Two source files now own what one was
trying to do.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
…r (arch-review #4)

The concrete defect the review's Before diagram names —
'joinContest -. overwrites .-> onConnect' — joinContest reassigned
client.onConnect while still connecting, destroying the singleton's
connect handler (status notification + reconnect reset + the broadcast
subscription) and leaving the parallel 'connected_once' callback as
dead code that nothing ever fired.

Concentrate room-lifecycle ownership in the singleton: the onConnect
handler now fires a one-shot 'ready' event after the broadcast
subscription, and joinContest registers on it instead of clobbering
onConnect. Cleanup is deterministic (one-shot self-remove + timeout
drop). Reconnect policy and message decoding already had one owner.

Public composable API is unchanged, so ContestRankingsView and
ContestAnnouncementBell need no changes. A regression test asserts
joinContest never reassigns onConnect and the ready hook drives the
room subscribe+publish.

(The full two-adapter transport seam — STOMP + in-memory — remains
the documented 'Worth exploring' follow-on; this commit ships the
verified defect fix rather than an unverified realtime rewrite.)

Validated: console vue-tsc 0 errors; useContestSocket spec 16/16.

Co-Authored-By: Claude <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jul 16, 2026
C3 (STOMP factory strong) — red team CR §3.2/§A.8/§A.9 verified the
premise is partially false: actual layout is one shared singleton + one
thin adapter + one self-managed connection, not 'two parallel stacks'.
Gating: (a) auth/topic verification of contest endpoints, (b) adapter
consolidation, (c) only then the factory abstraction.

C5 (Bookmark split worth exploring) — red team §7 priority #4 deferred
behind a SQL reorder migration (sort_order unique index + batch CASE
expression). Without that migration, the 'reorder becomes one write'
claim doesn't hold.

C7 (moderationStore god store worth exploring) — red team §1.8/§4
flagged that problems.ts (431 lines) is the same god-store shape and
must be bundled. Bundled scope is 935 lines + consumer migration +
Vitest rewrites; sequenced in independent sessions.

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 16, 2026
Complete date/time presentation deepening (arch review #4) on console side. Shared module + management adapter + contest views were already migrated; seven console personal/profile/subscription views still called raw toLocaleDateString/toLocaleString, re-learning locale + invalid + variant policy at each call site. Add console/src/utils/datetime.ts adapter (mirrors management, injects active i18n locale). Migrate UserProfileView, ProfileView, UserStatsPanel, UserProfileCard, SubscriptionView, ForumPostsView, SubmissionsView. NotificationsView's bespoke today-conditional stays local (app-specific). Type-check + eslint clean.
DavidHLP added a commit that referenced this pull request Jul 16, 2026
…xt composable

Architecture-review candidate #4 (safe subset). The contest-problem context
composable already owns the contest-aware state; move the repeated
contest-problem navigation action behind it too, so the ?contestId=
preservation lives with the rest of the contest routing invariants instead
of being copied per renderer.

- useContestProblemContext: add goToContestProblem(slug) owning the
  router.push({ name: "problem-detail", params: { slug },
  query: { contestId: currentContest.slug ?? "" } }) shape.
- ContestProblemShell / ContestProblemDock: goToPill delegates to
  ctx?.goToContestProblem(p.slug).
- ContestProblemShell drops its now-unused useRouter import + router decl
  (router was only used by the inlined push). ContestProblemDock keeps
  router/route (still used by the separate submissions-tab push).

Provider/inject wiring (ProblemContextKey, ContestProblemContextKey) is
untouched. pnpm type-check + ESLint pass.

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

useProblemLayout now receives the contestId ref as a parameter (threaded
from ProblemDetailView's single route.query.contestId derivation) instead
of re-reading the route, so the desktop layout, mobile layout, and contest
dock all consume one contest-mode signal. Adds a behavioural test for the
mid-session contest-entry reactivity and updates the contest spec to the
threaded seam.

Advances candidate #4 of the 2026-07-15 architecture review.
DavidHLP pushed a commit that referenced this pull request Jul 19, 2026
…s, type-safe tests

Two-axis code review of the 13-commit arch-review-20260718 branch
emitted 6 hard + 2 judgement Standards findings and 6 missing/partial
+ 5 scope-creep + 4 looks-wrong Spec findings. This commit closes
all hard Standards findings, 1 missing Spec, and the volatile-comment
documentation hygiene; defers 1 missing (Cand1 SCAN aggregation)
and 1 judgement (settings emit helper) to keep the fix-up focused.

Standards (6 hard):
- useSolutionAuthoring.publish: add isPublishing ref + re-entry guard,
  wire SolutionsEditView so the submit button is disabled and shows
  'Publishing...' while a create/update is in flight. i18n key
  'solution.editor.publishing' added in both locales.
- CommunityMembershipServiceImpl.joinCommunity: log loud if
  incrementMembers matched no row, so a deleted-in-flight community
  cannot silently leave the new membership without a counter bump.
  Leave path already checks affected rows.
- settings.ts FieldMapping: type the wireKey / dtoKey constraint to
  keyof W / keyof D, so a misspelled mapping entry fails to compile
  rather than silently dropping the field at runtime. The inner
  as Partial<W> / as D casts remain — TypeScript cannot prove value
  compatibility, but every key was already validated by the generic
  constraint.
- useSolutionAuthoring.spec.ts: type the mockRoute as a real
  RouteLocationNormalized (not 'as never'); useContestAuthoring.test.ts:
  type the createContest / addProblem mock returns as Contest /
  ContestProblem. Both are now real typed mocks at the public boundary.
- CommunityMembershipServiceImplTest: pin the Clock to a fixed
  Instant + ZoneOffset.UTC and assert joinedAt deterministically.
  Replaces the previous Instant.now() / ZoneId.systemDefault() which
  drifted between CI machines.
- Volatile review references removed from comments:
  'Architecture review candidate #3' (system-settings.ts:47),
  'residual-risk note in the task report' (useContestAuthoring.ts:272),
  'CreateContestDTO.java ~line 55' (contests.ts:88 + :229),
  'arch review 2026-07-10' (separator/index.ts in both apps),
  'arch review Card 7' (i18n/utils.ts), 'arch review #3' (date.ts),
  'arch review #4' (datetime.ts), 'arch review candidate #3'
  (storage.ts). All replaced with self-contained descriptions.

Spec (1 missing addressed + 1 documented):
- Cand3 clearCache: replace untyped Map<String,Object> with a typed
  ClearCacheResponseVO(List<String> clearedScopes, String timestamp)
  on backend (impl + service + controller), and update the two
  pre-existing tests to match. The frontend type was already
  correct; the contract is now typed at the API boundary.
- Cand2 StepScoringRule: comment updated to document that the
  Selector owns the on-mount default-pick (which needs the rules
  endpoint) rather than duplicating the fetch in the authoring module.

Verified:
- backend-spring mvn -Dtest=... test: 54/54 green (forum 33 + admin 21)
- console pnpm vitest useSolutionAuthoring.spec.ts: 9/9
- management pnpm vitest useContestAuthoring.test.ts: 16/16
- pnpm type-check clean on both frontends

Deferred:
- Cand1 QueueHealthSnapshot SCAN aggregation for failedCount /
  completedCount (still hardcoded 0L with TODO). The inFlight field
  is also missing. Both are still acknowledged in the plan; deferred
  to keep this fix-up focused.
- J1 settings emit helper extraction (judgement smell): 5 settings
  components repeat emit('update:settings', { [key]: value }); left
  in place; refactor would touch all 5 view files for marginal DRY.

24 files / +224 / -82. No pre-existing dirty worktree files modified.
DavidHLP added a commit that referenced this pull request Jul 20, 2026
Architecture-review 2026-07-19 candidate #4 noted both apps hard-coded
5 * 60 * 1000 ms as a local STALE_SESSION_MS constant, duplicating the
stale-session revalidation policy across console and management routers.

- Adds DEFAULT_STALE_SESSION_MS (5 minutes) to shared/auth-core/navigation
  and makes NavigationPolicyOptions.staleSessionMs optional. Callers that
  omit it now inherit the shared default automatically.
- Drops the local STALE_SESSION_MS constants and the explicit policy
  assignments in console/src/router/index.ts and management/src/router/index.ts.
- Adds three regression tests pinning the default value, the behavior when
  the option is omitted (revalidation still fires after 5 minutes, never
  silently degrades), and the override path.

The pre-edit comparison 'clock.now() - last > policy.staleSessionMs' would
have silently evaluated against undefined when a caller omitted the option,
disabling revalidation entirely. The local 'staleSessionMs' variable in
createNavigationPolicy closes that hole.
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