59 Commits

Author SHA1 Message Date
iye
9a122ffa27 feat(brand): update CSG logo and site title
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m20s
2026-06-02 11:00:14 +08:00
zyc
85cf284848 chore(otp): raise per-IP send-otp limit from 5 to 100 / 5min
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m59s
放宽同一出口 IP 5 分钟内可发送的验证码次数,避免办公网 / 校园网 / NAT
下多个真实用户互相挤掉配额。单手机号 60s 限频不变。

注意:当前 REDIS_URL 未配置,限流走进程内 Map,多副本部署时该阈值
按 pod 各自计数,实际放大为 N × 100。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 17:54:56 +08:00
iye
9772ba88ae fix(auth): 僵尸 JWT session 兜底 —— /api/me 返回 NOT_FOUND/UNAUTHORIZED 时自动登出
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m54s
NextAuth 用 JWT 策略,cookie 签名不会因 DB user 被删而失效,
导致 dev 清数据后浏览器仍显示"已登录"但拉不到任何数据(假登录)。

useSyncMe 现在识别 /api/me 的 401/NOT_FOUND/UNAUTHORIZED 三种信号,
命中后调用 signOut({ redirect: false }) + reset(),把 UI 切回未登录态。

生产环境不会清 user,主要受益是 dev/staging 重置数据后无需手动清浏览器 cookie。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 17:05:50 +08:00
iye
8b99c2f091 feat: 跨设备同步 + Logo v3 + 导航合并 + 窄屏适配 (v0.3.4)
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m44s
- useSyncMe: 登录后拉 /api/me 用 votedArtists 覆盖本地 store,登出清本地
- Providers: SyncMeBridge 接入 SessionProvider
- Logo v3 替换登录页/弹窗/Footer 的旧 <Logo> 组件
- Footer 删除 logo,简化为版权行
- Navigation 删除 mobile 第二行 NavLinks,合并到第一行
- NavLinks 统一布局,响应式 gap+字号(gap-5 sm:gap-8 / text-[13px] sm:text-sm)
- HeroVoteProgress 窄屏(< md)隐藏 12 格点,只留文字
- scripts/screenshot-narrow.mjs 验证脚本(可配宽高)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v0.3.4
2026-05-18 14:56:42 +08:00
iye
5c009f38cd fix(vote): 投票后立即 refresh ranking,不等 30s 轮询
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m48s
- useVoteAction 加 onVoteSuccess 回调,服务端 200 立即触发
- page.tsx + ranking/page.tsx 传 live.refresh
- 顺手把 fire-and-forget 改成 await + 失败回滚 + /api/me 跨设备对齐
- store 新增 rollbackVote + hydrateFromServer 两个 action

体感:本地 30s → 700ms,生产 30s → 150ms

bump to v0.3.3

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v0.3.3
2026-05-18 14:25:40 +08:00
iye
51009616a1 fix(home): Top12 出道位接 /api/ranking 显示真实票数
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m29s
- 首页 Top12Bar 之前只读 zustand store(初始 0 票),未登录访客永远看到"Awaiting Votes"
- 现在和 /ranking 页一样用 useRanking 30s 轮询,merge 服务端票数到本地 store(取 max)
- 与本地乐观投票兼容,投票后立即可见

bump to v0.3.2

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v0.3.2
2026-05-18 14:03:51 +08:00
iye
e05f63b94f chore(scripts): 补提交 reset-vote-data.mjs(清 DB 工具)
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m24s
清空 votes / fan_supports / daily_quota / sign_ins / risk_logs / snapshots
并 reset artists.vote_count=0 / current_rank=null。保留 users / artists / activity_config。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 13:55:55 +08:00
iye
80c37923d4 chore(release): v0.3.1 - 13 号虞浓氛围图 2 替换
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m6s
- TOS 缓存版本 7 → 8 触发 CDN 刷新
- CHANGELOG 加 v0.3.1 条目
- package.json 0.3.0 → 0.3.1

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v0.3.1
2026-05-18 11:00:35 +08:00
iye
338549ee27 docs(changelog): 每个版本附 commit hash + Gitea diff 链接
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m58s
- 修正 v0.1.0 tag 错位:从 8a83815(bootstrap) 改到 d5ed43a(2026-05-12 收尾)
- 每个版本补"Tag 打在哪 / 核心 commit / 完整 diff"三项链接
- 解决"点 v0.3.0 tag 进去看到的是 changelog diff,不是真正改动"的问题

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 20:24:19 +08:00
iye
93c3abe620 chore(release): v0.3.0 + 建立 CHANGELOG + 追溯版本号
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m56s
- 新增 CHANGELOG.md(SemVer 0.x 开发期规则 + 5 个里程碑)
- package.json version: 0.1.0 → 0.3.0
- 追溯打 tag:v0.1.0 / v0.2.0 / v0.2.1 / v0.2.2 / v0.3.0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v0.3.0
2026-05-15 20:21:12 +08:00
iye
10878ddb3f feat(vote): 重构投票模型为终身 12 票 + 每艺人 1 票
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
前端:
- store 改为 votedArtists[] + zustand persist
- VoteModal 删除 1/3/5/ALL 选择器,改三态(待投/已投/满额)
- 卡片/排行/详情页加 hasVoted 状态 + ✓ 角标
- Hero 右上角 Countdown 替换为 HeroVoteProgress(12 格点亮进度)
- /me 改为终身额度叙事(QuotaCard / StatsGrid / MyFanSupport)

后端:
- votes 表加 @@unique([userId, artistId])(已 apply 到生产 RDS)
- /api/vote 重写:12 票上限 + P2002 ALREADY_VOTED + P2003 NOT_FOUND 兜底
- /api/me 新增 votedArtists[] + voteQuota,移除 dailyQuota
- 新增 ERR.ALREADY_VOTED 错误码

测试:
- DB 层 5/5 + E2E 18/18 通过(scripts/e2e-vote-flow.sh)
- 修复 P2003 FK 违反未识别的 bug

详情见 docs/todo/voting-refactor-完成报告.md 与 voting-refactor-backend-完成报告.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 20:14:57 +08:00
iye
8d8451baa3 chore(tos): bump cache version 6 → 7 for 014-2 recrop
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m26s
The closeup studio cello shot (now atmosphere 2 after the swap)
had too much empty ceiling above the head. Recropped from
1440x2560 to 1440x1920 (3:4) with top offset 450px so the head sits
near the top of the frame and the full upper body is visible.

TOS upload: portraits/014-2.webp (overwrite).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v0.2.2
2026-05-15 18:40:11 +08:00
iye
aba9eee0c6 chore(tos): bump cache version 5 → 6 for 014 atmosphere swap
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m43s
14号 gallery atmospheres 1 & 2 swapped + atmosphere 1 recropped to 3:4
to frame the upper body. Same TOS URLs with new content, version bump
forces CDN/browsers to refetch.

TOS uploads: portraits/014.webp (overwrite, now wide-orchestra cropped
to 3:4), portraits/014-2.webp (overwrite, now closeup studio shot).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 18:35:19 +08:00
iye
85717d557d chore(tos): bump cache version 4 → 5 for 019-3 image recrop
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m12s
019 氛围图3 was pre-cropped from 9:16 (1440x2560) to 3:4 (1440x1920)
to remove the empty bamboo decoration above the head and frame the
upper body. Same URL with new content, so version bump forces CDN +
browsers to refetch.

TOS upload: portraits/019-3.webp (overwrites).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 18:29:07 +08:00
iye
034bb7ff42 chore(ui): drop nav logo + revert hero to default-muted
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m30s
- Navigation: removed top-left <Logo />, NavLinks now sit at the
  left edge. Logo component itself kept (still used by Footer /
  LoginForm / LoginModal).
- HeroBanner: restored simple "default muted + autoplay" behavior.
  The earlier try-unmuted-fallback flow was working but produced
  unpredictable first-paint audio depending on browser autoplay
  policy; muted is the safer default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 18:12:21 +08:00
iye
1236df31b8 chore(content): round 2 v2 corrections + new solo videos
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m32s
- 19 (方凌) cover correction: 氛围图3 → 氛围图2
  (original xlsx row was a "12号" typo for 19号 + wrong atmosphere
   number; corrected after teammate review)
- 13 (虞浓) joins CUSTOM_COVERS, will load portraits/013-cover.webp
  (image content = 虞浓_氛围图2)
- Bumped TOS_VERSION 3 → 4 — round-2 TOS uploads include overwrites
  of 006/007/014.mp4 and 019-cover.webp; new browsers/CDN entries.

TOS uploads (handled separately): 013-cover.webp, 019-cover.webp
(overwrite), 006.mp4 (overwrite), 007.mp4 (overwrite), 014.mp4
(overwrite).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 17:57:47 +08:00
iye
74a7b0ea16 feat(ui): polish hero/logo/cards + bump TOS version + drop missing-video flags
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m21s
- Hero eyebrow: "Top 12 · Virtual Idol Debut Project"
  → "Top 12 · Cyber Star Debut Survival"
- Hero video: attempt unmuted autoplay first, fall back to muted on
  browser autoplay-policy block (sound button reflects actual state).
- Logo: replace with cropped v2 art, drop purple drop-shadow glow.
- ArtistCard: drop non-top12 opacity dim AND the top dark gradient
  overlay — new high-quality portraits look better fully exposed.
- mock-data: 003/010/017/027/033 solo videos are present in v2,
  cleared MISSING_VIDEO set so the video section renders for them.
- tos: bump TOS_VERSION to 3 — videos/portraits overwritten on TOS,
  this cache-busts older URLs in browsers and CDNs.

TOS uploads (handled separately): hero-pv.mp4, 5 solo videos
(003/010/017/027/033), 7 cover images, 6/036 atmosphere images.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 17:02:29 +08:00
iye
49be38ff77 chore(content): apply v2 artist adjustments
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m49s
- 021 (温景然) enName RYAN → KINGSTON
  (fixes duplicate: 006 stays RYAN, 021 was a typo)
- 002/003/005/012/014/019/025 now use portraits/{no}-cover.webp
  for the cover (chosen from v2 atmospheres per 调整.xlsx). Gallery
  remains {no}/{no}-2/{no}-3 as before.
- 036 third atmosphere image is now expected (v2 provides it),
  removed MISSING_ATMOSPHERE_3 set.
- tosUrl now appends ?v=2 cache-buster so overwritten TOS files
  refresh immediately instead of waiting on CDN/browser cache.

Note: this commit is paired with TOS uploads (handled separately).
2026-05-15 15:56:13 +08:00
zyc
7168e50a6e fix: prod login + env-file driven config + scroll-snap bounce
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m54s
- env: 解封 .env / .env.production 提交, 仅忽略 .env.local 系列;
  .env.production 承载 DATABASE_URL / AUTH_SECRET / AUTH_URL /
  SMS_* / NEXT_PUBLIC_TOS_DOMAIN, Dockerfile runner 阶段 COPY 进
  运行时镜像, Next.js standalone 启动自动加载
- ci: 移除 kubectl 注入 secret 步骤(env 已烧入镜像), 保留占位避免
  envFrom optional 引用告警, 修复 /api/auth/providers 500 (缺 AUTH_SECRET)
- auth: signIn 失败透传 NextAuth 真实错误码, 不再被"验证码错误"一刀切掩盖
- home: 首页 scroll-snap-type 由 mandatory 改 proximity, 修复滚动到
  底部被强制吸回候选区顶部的回弹

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.2.1
2026-05-14 17:31:00 +08:00
iye
f6177fc542 chore(env): add local .env.local (auth + SMS keys) for team sharing
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m6s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 17:09:50 +08:00
iye
6759d6a689 chore(env): add local .env (DATABASE_URL) for team sharing
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m52s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 17:02:48 +08:00
iye
3f5d33c422 fix(ui): merge nav + sticky filter into a single backdrop-filter band
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7m27s
Two adjacent backdrop-filter elements (nav at y=0-80, filter at y=80+)
always show a visible seam at their boundary because each filter clips
its own blur kernel, so the edge pixels sample slightly different
neighborhoods. Same recipe doesn't help — it's a structural issue.

Fix: when filter is stuck, render an absolutely-positioned glass child
inside the filter that extends from -top-20 to bottom-0 (i.e. covers
nav area + filter area as ONE element). Nav reads filterStuck from a
tiny shared zustand UI store and disables its own glass layer in that
state, so only the shared band is visible. Single element, single
backdrop-filter, no seam.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 12:42:03 +08:00
iye
ed222d1c5f feat(ui): scroll-aware nav glass + floating back button + hero polish
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m17s
- Navigation: fixed + transparent over Hero (home) / at page top (other routes);
  fades to glass-on-scroll. Glass uses surface tone matching site cards.
- Filter bar sticky glass synced to nav recipe (no seam between layers).
- HeroBanner: full-viewport video, center title removed, bottom dim overlay
  removed, eyebrow/countdown repositioned below the nav.
- ArtistDetail: removed portrait shadow; added FloatingBackButton that uses
  router.back() with internal-history fallback to /.
- Floating buttons (back + vote) translateY upward to avoid footer rather
  than disappearing, via useFooterPush.
- Home: useScrollRestore preserves scroll position on return from detail
  pages; temporarily disables scroll-snap during restore.
- PerformanceVideo: max-w capped by 85svh*16/9 so small viewports never crop.
- ArtistFilters: hide horizontal scrollbar thumb in tag container.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 12:25:54 +08:00
iye
1073262e12 ci(secret): inject Aliyun SMS credentials into cyberstar-env
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m44s
线上 /api/auth/send-otp 返回 SMS_NOT_CONFIGURED, 因为 pod 的
process.env 里没有 SMS_*。沿用 DATABASE_URL 已有的硬编模式,
把 4 个短信变量也写进 workflow 的 kubectl create secret 步骤。

后续 pod rollout restart 已在原 workflow 中自动触发,
重启后 envFrom 会重新读到新 Secret。
v0.2.0
2026-05-13 19:33:00 +08:00
iye
cfd44403cb fix(deploy): inject NEXT_PUBLIC_TOS_DOMAIN at docker build time
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m17s
线上 https://cyberstar.airlabs.art 立绘 + 视频全部缺失, 因为部署镜像里
NEXT_PUBLIC_TOS_DOMAIN 是空字符串, 触发 tosUrl() fallback 走相对路径
(/portraits/001.webp 等), 而 public/portraits 已经 .gitignore 不入镜像 → 全 404。

根因: Next.js 把 NEXT_PUBLIC_* 编译进 client bundle, 必须 build 时注入,
运行时通过 envFrom secret 注入无效。

修复:
- Dockerfile builder 阶段加 ARG NEXT_PUBLIC_TOS_DOMAIN + ENV, 在 next build 前生效
- .gitea/workflows/deploy.yaml docker build 步骤加 --build-arg NEXT_PUBLIC_TOS_DOMAIN=...

推送后 CI 自动重建镜像, 部署后 HTML 里 src 会变成完整 TOS URL。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 19:03:35 +08:00
iye
a9f4799f71 feat(db): wire real persistence for votes / users / quota / supports
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m26s
数据正式落库, 不再仅靠浏览器内存:

prisma/schema.prisma:
- Artist 模型对齐当前前端数据形态:
  * 旧字段 slogan / birthday / cv / themeColor 改为可选 (前端早不用, 但保留兼容历史 seed)
  * 新增 age / gender / motto / personality / catchphrase / skills / track (来自人物小传)
- 注释从 "001 ~ 035" 改 "001 ~ 036"

prisma/seed.ts:
- 整体重写: 从 src/lib/artist-bios.ts 的 ARTIST_SEEDS 灌真实 36 人
- 不再写假数据 (AURORA / LUMI / NEBULA...)
- portrait / videoUrl 不入库 (前端 NEXT_PUBLIC_TOS_DOMAIN 拼接, 换桶不用 reseed)
- ActivityConfig 默认 dailyQuota=10, perArtistLimit=0, voteEnabled=true, 活动期 30 天

src/lib/date-utils.ts (新增):
- startOfUtcDay(): 修复"今日"在 MySQL @db.Date 列与 JS Date 之间的 TZ 漂移
- isSameUtcDay(): 共享给签到判断

修复 P2025 bug (vote / me / signin):
- 用 startOfUtcDay 替代 startOfDay (后者用 setHours 取本地午夜,
  对 @db.Date 列会因 TZ 漂移导致 upsert 后再用 userId_date 复合键查找失败)
- /api/vote 的扣额度从 userId_date 改用 dq.id 主键 update, 双保险
- 三个路由的 startOfDay 重复实现合并到 lib/date-utils

E2E 验证 (curl):
  登录 → 投 5 给 002 → 余 5 ✓
  投 3 给 003 → 余 2 / totalVotes 8 ✓
  /api/me supports 反映 002+003 真实 voteTotal ✓
  超额 (5 票 余 2) → 409 QUOTA_EXHAUSTED ✓
  /api/ranking 票数实时反映 DB ✓

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 17:32:38 +08:00
iye
58da508e7d chore(env): switch TOS bucket from teammate's to own (cyber-star@cn-shanghai)
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m57s
域名变化:
  旧: https://cyberstar.tos-cn-shanghai.volces.com/cyber-star (临时桶, 带路径前缀)
  新: https://cyber-star.tos-cn-shanghai.volces.com         (自有桶, 桶名即子域, 无路径前缀)

操作:
- 在自有火山账号下建 cyber-star 桶 (cn-shanghai), 公共读, 对象 ACL 默认公共读
- 网页控制台「上传文件夹」方式把本地 assets-compressed/portraits 和 videos 直接传到桶根
- 切 NEXT_PUBLIC_TOS_DOMAIN

代码无需改动 (tosUrl() 已自动处理), 仅 env 切换。
next.config.ts images.remotePatterns 已包含 *.tos-cn-shanghai.volces.com, 无需更新。

dev 验证:
- 首页 / artists/001 / artists/036 / ranking 所有资源 src 全部走新桶
- 旧域名已彻底切除
- portraits/001.webp 200, hero-pv.mp4 200, 缺视频/缺氛围图 3 仍然 404 (预期)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 16:14:58 +08:00
zyc
b3bdb60c81 ci: inline DATABASE_URL in workflow (volcano RDS internal endpoint)
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m0s
Matches the AirGate convention of putting infra credentials directly in
the deploy yaml — no Gitea Secrets configuration required, push-to-deploy
just works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:28:55 +08:00
zyc
2c3357e33d ci: trim cyberstar-env Secret to DATABASE_URL only
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Previous commit scoped too broadly. Other env vars (TOS/SMS/WECHAT/etc.)
already have application-level fallbacks and aren't required to make the
deploy work, so they don't need to be in the workflow yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:27:41 +08:00
zyc
19e789d6ac ci: sync cyberstar-env Secret from Gitea repo secrets
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m32s
Previously cyberstar-env had to be created manually with kubectl, which
broke the "git push = full deploy" expectation. Workflow now derives the
runtime Secret from Gitea repo secrets each deploy, so DATABASE_URL,
AUTH_SECRET, TOS/SMS/WECHAT credentials etc. are kept in one place and
applied transactionally with the rest of the manifests.

Repo secrets that need to exist in Gitea Settings:
  DATABASE_URL, REDIS_URL, AUTH_SECRET,
  TOS_ENDPOINT, TOS_REGION, TOS_BUCKET, TOS_ACCESS_KEY, TOS_SECRET_KEY,
  NEXT_PUBLIC_TOS_DOMAIN,
  WECHAT_APP_ID, WECHAT_APP_SECRET,
  SMS_ACCESS_KEY, SMS_SECRET_KEY, SMS_SIGN_NAME, SMS_TEMPLATE_CODE,
  HCAPTCHA_SITE_KEY, HCAPTCHA_SECRET

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:25:47 +08:00
iye
9d003a3b6f fix(auth): persist generated OTP so real codes can verify (with in-memory fallback)
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
之前 send-otp 生成的码只在 Redis 可用时才存. dev 没配 Redis → 码生成即丢, 用户拿到
真实 SMS 验证码也登不进去 (除了万能码 123456). 这是上次"修任意 6 位绕过"留下的回归.

新增 src/lib/otp-store.ts:
- storeOtp / consumeOtp 双方法, 内部按 Redis 可用性自动路由
- Redis 可用 → 走 Redis (生产)
- Redis 缺失 → 走进程内 Map (dev / 联调), 通过 globalThis 抗 HMR
- consumeOtp 校验通过即 del, 防重放

send-otp 与 verifyOtp 改走 otp-store, 不再直接读写 Redis 句柄。

E2E (curl + NextAuth callback):
  发码 → dev 日志拿 code=209988
  错码 000000 → 拒绝, session=null
  真码 209988 → 通过, session=粉丝_0099
  重放 209988 → 拒绝 (一次性消费)

并在 NODE_ENV !== production 时把生成的 code 打到 dev 终端, 方便 QA。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 15:21:00 +08:00
iye
8597957af4 fix(auth): reject wrong OTP codes when Redis is missing (security)
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m8s
Bug: verifyOtp 里 dev 态 Redis 未配置时, 写了 /^\d{6}$/.test(code) 作为联调 fallback,
导致任意 6 位数字都能登录(包括恶意构造). 实际表现: 用户输入错误验证码也能直接登录。

修复:
- Redis 未配置时无论 dev/prod 一律拒绝, 不再做"任意 6 位"放行
- dev 联调若需要绕过短信, 用万能码 123456 (已保留, 仅 NODE_ENV !== production)

E2E 验证 (curl + NextAuth credentials callback):
  错误码 999999 → /login?error=CredentialsSignin , session=null ✓
  万能码 123456 → callbackUrl=/, session 有用户 ✓

新增 tools/test-verify-otp.mjs 作为该 bug 的回归测试。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 15:08:03 +08:00
iye
0a7c1ec130 feat(auth): wire Aliyun SMS provider for phone OTP login
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m33s
接入流程:
- src/lib/sms.ts: 封装 sendOtpSms(phone, code), 走 dysmsapi.aliyuncs.com 全局端点
- /api/auth/send-otp:
    * 生成 6 位验证码 → Redis 5min TTL
    * 调 Aliyun SDK 发送; OK → 200, isv.* 错误 → 422, 其它 → 500
    * SMS_NOT_CONFIGURED 时 dev 仍能 console.log 验证码联调
- auth.ts verifyOtp:
    * dev 万能码 123456 保留
    * 否则 redis.get(sms:otp:phone) 比对, 通过后 del 防重放
    * Redis 未配置时 prod 拒绝, dev 接受任意 6 位

环境变量 (.env.local, 不入仓库):
- SMS_ACCESS_KEY / SMS_SECRET_KEY (RAM 子账号)
- SMS_SIGN_NAME (例: 广州气元科技)
- SMS_TEMPLATE_CODE (例: SMS_506210397)

依赖:
+ @alicloud/dysmsapi20170525
+ @alicloud/openapi-client
+ @alicloud/tea-util

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 14:56:47 +08:00
iye
15af8e1781 fix(image): disable next/image optimization for TOS-hosted assets
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m10s
桶里资源已是 webp + 预压缩 + CDN, 不需要 Next 16 再做一次 image 优化:
- images.unoptimized=true: 直接走 <img src="https://..."/>, 浏览器走自己网络栈
- 顺带绕过 dev 时 fake-IP 代理 (Clash/V2Ray TUN 返回 198.18.0.x) 触发的
  "resolved to private ip" 拦截
- remotePatterns 保留 (Next 16 即便不优化仍校验域名白名单)

生产 CPU 占用也省了, 不需要在 origin 再编码 webp。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 14:43:52 +08:00
iye
8c88943a06 feat(tos): point all static assets to volcano TOS bucket
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m0s
资源已上传到 https://cyberstar.tos-cn-shanghai.volces.com/cyber-star/
代码改动:
- 新增 src/lib/tos.ts 提供 tosUrl(path) 工具,读 NEXT_PUBLIC_TOS_DOMAIN
- mock-data.ts: portrait/gallery 切到 .webp, videoUrl 走 TOS, 全部通过 tosUrl()
- page.tsx Hero PV 走 tosUrl("videos/hero-pv.mp4")
- next.config.ts 把火山 TOS 域名(沪/京)+ 火山 CDN 加进 images.remotePatterns 白名单
- .env.example 更新 NEXT_PUBLIC_TOS_DOMAIN 示例为实际桶域名

体积影响 (与之前打包给运维的 cyber-star-assets.tar.gz 一致):
- 立绘 5MB png → 100-300KB webp (-95%)
- 单人 solo 5-10MB mp4 → 1-3MB (-70%)
- Hero PV 45MB → 12MB (-70%)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 14:37:46 +08:00
iye
c0bce80dd1 chore(tools): asset compression pipeline for TOS bucket upload
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m13s
新增 tools/asset-pipeline/ 用于把 public/portraits & videos 压缩成桶友好体积:
- sharp:    PNG → WebP q82, 最大宽 1600 (-view 三视图 2400)
- ffmpeg:   MP4 → libx264 CRF 28, 最大宽 1920, AAC 96k, faststart
- pack.mjs: tar -czf 整目录 → cyber-star-assets.tar.gz

效果 (146 portraits + 33 videos):
- 立绘:  768.6MB → 26.8MB  (-96%)
- 视频:  251.7MB → 76.1MB  (-70%)
- 总计:  1020MB  → 103MB   压缩到 1/10, 95s 跑完

输出位于仓库外 ../assets-compressed/ 与 ../cyber-star-assets.tar.gz, 不入 git。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 14:26:42 +08:00
zyc
7506372abd fix(ci): hoisted node_modules + alpine binary target for Prisma in Docker
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m45s
Root cause (from build log):
1. Prisma 6 generates client into @prisma/client package dir (not .prisma/client)
2. pnpm default isolated linker puts everything in .pnpm/ store with symlinks
   at top-level — Docker COPY of @prisma followed broken/incomplete symlinks
3. node:22-alpine needs linux-musl-openssl-3.0.x engine binary

Fixes:
- .npmrc: node-linker=hoisted → flat node_modules, COPY behaves like npm
- schema.prisma: add linux-musl-openssl-3.0.x to binaryTargets
- Dockerfile: drop dead .prisma/client checks, copy only @prisma (where
  Prisma 6 actually writes the client) plus standalone output

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:13:37 +08:00
iye
409c4c4b50 docs(ops): TOS bucket + Aliyun SMS integration spec for team handoff
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m8s
后端 / 运维同学填回 yaml 模板后,前端 (Claude) 直接接入。
内容覆盖:
- TOS 桶配置 / 目录结构 / 公共读 / CDN
- 阿里云短信签名 + 模板 + RAM AK
- Redis / MySQL / NextAuth 上下游依赖
- 验收清单 + 时间线
- 敏感信息 (AK/SK) 走密钥管理,不入文档

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 14:07:54 +08:00
zyc
d2b8c0afdc fix(ci): explicit prisma generate + ignore-scripts in Docker build
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Root cause: pnpm 10+ skips lifecycle scripts in root/CI environments by
default, so the project's postinstall (prisma generate) never ran. The
runner stage then failed to COPY node_modules/.prisma because builder
never produced it.

Fix: install with --ignore-scripts, then call `pnpm exec prisma generate`
explicitly in both deps and builder stages, with ls assertions to surface
any future regression early.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:06:47 +08:00
zyc
6155638549 fix(ci): bump base image to node:22-alpine
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m21s
corepack-installed pnpm 11 requires node:sqlite (Node 22+).
Build was failing in deps stage with ERR_UNKNOWN_BUILTIN_MODULE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:58:47 +08:00
iye
c3863a4dab Merge branch 'main' of https://gitea.airlabs.art/zyc/UI-UX
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
2026-05-13 13:58:22 +08:00
iye
71a2672ff6 fix(data,ranking,ui): real dynamic ranking + data sync hardening
数据准确性
- 票数初始为 0,不再用 Math.sqrt 公式造假票
- 排序 tiebreaker 统一为 votes desc + no asc,确保稳定
- store.rank() 与 sortArtists() 行为对齐

Top12 / 出道位
- Top12Bar 仅收纳真正有票的人(votes > 0),0 票时显示"出道位尚未产生"空态
- ArtistCard / RankingRow / SearchModal / MyFanSupport / RankCard 的 inTop12 高亮全部加 votes > 0 守卫
- ArtistFilters 新增"实时排名 / 编号顺序"分段切换 + 首页 sortKey 状态

领奖台 (Top3Podium)
- 1 人有票即可显示领奖台(此前要求 >= 3 人才显示)
- 缺位的 #2/#3 用"虚位以待"占位卡片填充,与正式卡片同 3:4 比例对齐
- 全员 0 票时三张全部显示虚位以待
- 卡片间距拉大到 gap-8 sm:gap-12

排行榜页 (/ranking)
- API 票数与本地乐观投票取 max() 合并,避免 API 落后覆盖本地新票
- 合并后重新排序与赋 rank
- 仅 >= 12 人有票才显示出道线分隔与复活位标记
- 复活位 gapToDebut 计算修正

跨日额度
- selectRemaining 按 quotaDate 判断是否跨日,跨日自动回满额(此前会卡在昨日剩余值)

搜索 (SearchModal)
- 改为订阅 store 拿活的票数,投票后立即反映
- 默认"热门 Top12"只在真正有票时显示,否则降级为"推荐艺人 · 编号顺序"
- 票数显示统一走 formatVotes(0 票不再显示 0.0w)

人物详情
- 36 人真实数据接入,移除全部静态数据(slogan/birthday/cv/themeColor)
- 接入人物小传 docx 数据(年龄/身高/性格/口头禅/技能/赛道/座右铭/长简介)
- 视频区与版心同宽 + 首帧自动作为封面 + 整区点击播放/暂停 + 可拖拽进度条
- 表演图片改为三张氛围图竖向 3:4,左对齐
- 移除分享按钮,投票按钮全宽

个人页 (/me)
- 移除等级/邀请好友/签到/编辑资料等静态数据
- 退出登录按钮在移动端 icon-only 显示(此前 sm:hidden 导致移动端无法登出)
- 我的应援 list 基于真实 myVotesByArtist 派生,凯之类的投票真正同步过去

导航
- 余票徽章未登录态显示 0/0,已登录显示 N/10
- 登录/注册按钮样式与登录后头像胶囊保持一致(紫色实心)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 13:56:42 +08:00
zyc
c19b3b7b05 ci: add CI/CD pipeline for cyberstar.airlabs.art
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m41s
- Dockerfile: multi-stage Next.js standalone build with pnpm + prisma
- k8s manifests: single web deployment + Traefik ingress + LE TLS
- Gitea workflow: build/push to Volcano CR, deploy to K3s, log-center failure reporting
- next.config: enable standalone output for slim container image

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:44:04 +08:00
iye
d5ed43acbd feat(ui): design overhaul, global login modal, design spec
- nav: center links (首页/排行榜/我的), right-side AuthMenu + RemainingVotesBadge; image logo with responsive sizing
- auth: replace /login route with global LoginModal triggered anywhere; "我的" intercepts unauth users with post-login redirect
- home: full-screen Hero, redesigned Top12 (12 pill cards, top-3 glow), scroll-snap mandatory between Hero/Top12/candidates
- home: candidates section with sticky filter that gains frosted-glass bg when stuck (matches nav)
- filter: simplified tags (全部/舞蹈/声乐/rap/全能型); ArtistCard uniform purple vote button
- ranking/me: remove Top12Bar; me header stacks 编辑资料/退出登录 vertically
- typography: font-logo set to Orbitron; ✦ glyph in CYBER ✦ STAR preserved
- layout: max-w-[1500px] unified across pages
- docs: add design-spec.md + design-spec.html with full visual spec (lucide SVG, zero emoji policy)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
v0.1.0
2026-05-12 18:59:30 +08:00
iye
bd5a361a18 feat(vote): remove all voting limits (no daily quota, no per-artist cap, unlimited votes) 2026-05-12 14:15:50 +08:00
iye
9fe9fa914f fix(ux): center modals with overlay; live vote with toast; deterministic mock data; cascade layer fix 2026-05-12 14:09:31 +08:00
iye
7949f9bcd1 fix(nav,auth): trim nav to wireframe pages; auth gracefully degrades when DB unavailable in dev 2026-05-12 10:29:02 +08:00
iye
854a162109 feat(live): real-time ranking polling hook + LiveBadge, ranking page falls back to mock when API unavailable 2026-05-12 10:06:16 +08:00
iye
b7fbd5ac53 feat(auth): Auth.js v5 with phone OTP login, send-otp API, login page and user state in nav 2026-05-12 10:03:58 +08:00
iye
175276a085 feat(api): add REST API routes (artists/ranking/me/vote/signin) + Redis rate limiting + Zod validation 2026-05-12 09:59:38 +08:00