浅念
63b3e539fd
docs: register asset status acceptance — admin branding tab shows current banner/cover/logo
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-23 10:56:14 +08:00
浅念
2618ad3163
feat(assets): album asset status — show current banner/cover/logo in admin branding tab
...
- 契约 listAlbumAssetStatus(GET /albums/{albumId}/assets):每 (kind,locale)
最新 effective 行 + 现签 URL;权限同详情(可见即读);deleted → 404
- 服务端 AssetsService.status(createdAt,id DESC 组内取首;presignGet 现签)
- Admin 品牌资产页签改造:进入即加载现状——三种 kind 卡片(预览图/未设置
空态/设置时间),banner 分全局/zh/en 槽位;上传成功自动刷新(替换语义)
- API 56/56、admin 87/87 + build 全绿
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-23 10:51:53 +08:00
浅念
7385785504
feat(i18n): T15 full-stack language audit with coverage assertions
...
- viewer/admin 错误码→文案映射按契约 ErrorCodes 枚举全集登记(机器守护
spec 遍历枚举断言无缺漏 + 映射键在 zh/en 词条均存在 + locale 键集
递归相等);mobile 镜像补齐 DOWNLOAD_DISABLED(P4a 增补漏登)
- 漏译修复:viewer.photoRemoved en 侧缺失(审计测试捕获);
DOWNLOAD_DISABLED 观众侧专属文案 + useDownload 专属提示接线;
admin apiMsg 增 RATE_LIMITED/ALBUM_NOT_PUBLIC/DOWNLOAD_DISABLED/
SYNC_AUTH_FAILED 四码词条
- 运行时抽查:admin 主旅程 en 版 e2e(登录→相册管理全英文 + 偏好
持久);embed iframe lang=en 渲染与深链跟随 e2e(zh 侧既有用例覆盖)
- contracts 新增 ./errors 浏览器安全子路径(errors.ts 纯 TS);
admin 增 @photo/contracts devDep(仅测试消费)
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-20 23:44:27 +08:00
浅念
ddb4c8175f
feat(embed): versioned loader with postMessage whitelist
...
packages/embed-loader:手写零依赖加载器(esbuild bundle+minify→dist/v1/loader.js,
IIFE target es2018 4.7KB)。自发现 EMBED_ORIGIN(currentScript 兜底扫描)、
data-slot 必填缺失静默 no-op、__photoliveEmbedV1 标记去重+同槽去重、
窄屏 <480px 强制 sm+默认收起、postMessage 双向白名单(source+origin 严格
校验,resize 夹取 60-640)、sessionStorage 收起记忆、零全局零 console
全静默。sync 脚本携 sha256 校验拷贝至 viewer publicDir(两份产物入库);
src/params.js 为 loader/单测共用参数源(11 vitest 用例)。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-20 22:31:40 +08:00
浅念
f08d68bebf
feat(admin): batch ops, transfer, watermark, usage and qr share pages
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-20 21:10:30 +08:00
浅念
452ec12bbd
test(sync): cross-region simulation integration suite
...
P2 Task 11: 六幕串行场景套件(cn+ov 双入口进程内起真服务,真实双
MySQL/MinIO/HMAC/WS)——①端到端复制+互动+统计回传(A25 观众路径只打
ov、A27 快照替换)②隐藏传播(high 优先+WS photo.hidden+恢复发布复原)
③发送器停机积压→排空追平→对账稳态④墓碑不复活(旧 published 重放拒绝
+bootstrap 重跑仍隐藏,A26)⑤紧急停用(ov 404/WS error、cn 无感、enable
复原含新照片)⑥50 并发点赞/30 并发浏览幂等小样(终值==Σdelta、恰+15)。
services/sync devDeps 增 ws/@types/ws/ioredis(WS 场景客户端 + 限速
滑窗键清场,同 api spec 约定;版本与 api 对齐)。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 20:05:25 +08:00
浅念
85b2da3db5
feat(ov): idempotent batched likes and realtime broadcast
...
P2 Task 7(点赞批次端点 + ov 实时网关):
- interactions 模块(仅 ov 装配):POST /public/albums/{token}/photos/
{photoId}/likes——相册不可见/照片未发布 404、delta 1..20 与
sessionId/batchId 1..64 校验 400;单事务 like_batches 唯一插入
(P2002=幂等重试,batchAccepted=true 不重加)→ photos_ov.likeTotal/
countVersion 原子自增回读 → outbox_stat_events(kind=likes) 兜底;
Redis ov(REDIS_URL_OV)INCRBY+TTL 会话/IP 双窗口限流(默认
500/2000,超限 429 RATE_LIMITED),幂等重试先于限流不耗额度
- 过期批次重放边界(24h 清理后的 batchId 重插会再次计入):接受该
边界——无永久账本可查(保留全量批次行违背清理策略),会话/IP 窗口
上限即重放天然封顶(同一 sessionId 重放挤占自身额度),偏差已在
likes.service 头注释登记,Task 8 统计侧按清理后语义处理
- realtime:ContractWsAdapter(契约 {type,...} 帧分发,ws 与 HTTP 同
端口挂 /v1/realtime)+ RealtimeGateway 相册房间模型——subscribe 逐
相册校验 isAlbumVisibleOv,失败回 error 帧且连接保持;应用层 ping→
pong 回显 ts;传输层 HEARTBEAT_MS 周期 ping 踢死连接;点赞提交经
EventEmitter2(单进程,多实例 Redis pub/sub 登记 P5)触发
likes.burst 500ms 合并窗(delta 聚合、originBatchIds 供客户端过滤
自身批次);sync-inbox 应用事务提交后发 OV_PHOTO_APPLIED →
photo.published/hidden/deleted 按房间广播(stale/duplicate/暂存/
缺席即撤下不转发)
- 契约:REALTIME_WS_PATH、ServerMessage 增 error 帧(穷尽锁 7 成员
+ 类型测试)、LIKE_SESSION_WINDOW_DELTA/LIKE_IP_WINDOW_DELTA/
LIKE_BURST_MERGE_MS 常量与 .env.example
- e2e(likes-realtime.e2e-spec.ts,6 用例):A09 三批累加/幂等重试/
outbox 行、小额度 429、ping/pong+幽灵 token error 帧连接保持、
A10 双房间隔离(先证非空调订)、合并窗单 burst(delta 求和+双
originBatchIds)、cn 隐藏→房间收 photo.hidden+隐藏后点赞 404;
凡进程内起 AppOvModule 的 spec(sync×2/public-ov/emergency)补装
ContractWsAdapter 与生产 main-ov 一致——api e2e 168(162+6)
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 17:51:55 +08:00
浅念
65726702f3
feat(sync): cn event coverage, bootstrap and outbox sender
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 15:21:41 +08:00
浅念
3d6e9ce175
feat(ov): ov schema, entry skeleton and compose profile
...
P2 Task 2 ov 数据层:
- compose ov profile:mysql-ov(mysql:8.4,127.0.0.1:3309,库级
utf8mb4_0900_bin)+ redis-ov(6381),cn 侧默认 up 不受影响
(3307/3308 已被其它本机项目占用,端口决策 3309)
- @photo/db-ov:9 表 schema(albums_ov/photos_ov 副本、sync_inbox/
sync_state 同步账本(水位行同表标志位)、like_batches/album_stats/
visit_sessions/visit_requests 互动域、outbox_stat_events 回传队列);
迁移 20260918060102_init_ov 走固化校对手决流程,information_schema
复核全部业务表/列 utf8mb4_0900_bin(_prisma_migrations 同 cn 先例
保持 unicode_ci)
- generator 独立 output(包内 generated/):pnpm 将两包 @prisma/client
提升为同一 store 实例,默认 .prisma/client 输出会被双 schema 互相
覆盖,ov 客户端独立产出经 src/index.ts 转出口
- ov 入口骨架:main-ov.ts(PORT_OV 默认 8081)+ app-ov.module.ts
(仅管线骨架 + /v1/health,feature modules 后续任务装配);logger
装配选项抽取至 shared/logger-options.ts 供 cn/ov 双入口共用(行为
不变,P1 套件守护)
- test/reset-ov.ts:ov 库 truncate-if-exists 隔离工具(无 MinIO 清理)
- 根脚本:db:generate 双库齐跑,新增 db-ov:migrate/db-ov:generate/dev:ov
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 14:11:41 +08:00
浅念
a6cc190751
feat(admin): art-design-pro integration with local session auth
...
P1b Task 9: apps/admin based on art-design-pro v3.0.2 (tag, commit
d224dd4; source recorded in apps/admin/SOURCE.md), trimmed to
layout/login/i18n/request/table-form core. Login wired to real
POST /v1/auth/password (single-token session), Bearer header on all
requests, 401 interceptor logout+redirect, request-driven sliding
renewal (refresh at ~80% TTL, single-flight), best-effort
POST /v1/auth/logout on user-menu logout. One protected stub page
(相册管理). Dev port 5176 strictPort + /v1 proxy to 8080. Admin keeps
its own eslint9 flat config; root eslint ignores apps/admin/**.
vitest unit tests for renewal pacing + Playwright e2e (guard redirect,
localized wrong-password error, login lands on stub, token persists
across reload, logout clears session).
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 11:47:32 +08:00
浅念
d0b79dd0c9
feat(viewer): waterfall feed with lazy load, filter and new-photo hint
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 10:23:27 +08:00
浅念
adc45b1563
feat(viewer): album page with locale fallback and timezone display
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 09:52:22 +08:00
浅念
f9cb72ed06
feat(viewer): vite vue3 scaffold with routing and i18n
...
- apps/viewer (@photo/viewer): Vite 5 + Vue 3.4 + TS 最小骨架(无 Pinia/UI 库)
- router: /a/:token 活动页 + /a/:token/p/:photoId 单图深链(复用活动页组件)
- i18n: vue-i18n 9,zh-CN/en 词条,localStorage(viewer.locale) 持久 +
navigator.language 检测兜底 zh-CN;顶栏语言切换组件;文案全量走 i18n
- api client: 类型化 fetch 封装(契约生成类型直连),错误信封 → ApiError,
错误码 → i18n key 映射表(src/api/errors.ts)
- scripts: dev/build/preview/test(vitest 10 例)/test:e2e(Playwright 冒烟,
webServer 自拉 dev)/lint/vue-tsc 全部接入 turbo
- root: +@playwright/test、+eslint-plugin-vue(flat config 收敛到 apps/**/*.vue)、
pnpm.onlyBuiltDependencies 放行 esbuild
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 09:12:45 +08:00
浅念
6863c736c3
feat(api): public album info with locale fallback
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 23:33:57 +08:00
浅念
64a2cec788
feat(worker): sharp pipeline with orientation, variants, exif strip
...
Task 20: services/worker package (@photo/worker, commonjs) with jest
unit/e2e configs mirroring api conventions (root .env via test/load-env.ts,
moduleNameMapper to @photo/contracts/db-cn/storage TS sources, maxWorkers 1,
test/reset.ts = MySQL TRUNCATE + MinIO photo-live/dev prefix wipe) and the
sharp processing pipeline runPipeline(): metadata whitelist gate
(jpeg/png/webp/avif/heif/tiff, else PipelineError UNSUPPORTED_TYPE),
decode failures mapped to PipelineError DECODE_FAILED, no-arg .rotate()
auto-orientation (EXIF 5-8 swaps stored width/height), thumb <=480w q75
and browse <=1920 q82 JPEG variants with withoutEnlargement, and no
withMetadata so outputs carry no EXIF/GPS at all. sharp 0.35 prebuilt
loads script-free under pnpm 10; its win32 libvips lacks a HEVC encoder,
so the HEVC-HEIF unit case self-skips with a console note (G2 verifies
with real iPhone samples) while the AV1-HEIF case covers the same
libheif decode path / 'heif' format bucket.
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 23:16:20 +08:00
浅念
a2a3ccd340
feat(api): idempotent upload session init with presigned credential
...
POST /v1/uploads (Task 18, A03): @@unique(userId, clientTaskId) idempotency
anchor — same params return the original session (201, no duplicate photo);
param drift (album/bytes/contentType/extension) -> 409 UPLOAD_SESSION_CONFLICT.
photos.objectKey is set to the FINAL originals key at init while the session
holds the staging key; IDs come from an app-side cuid-like generator so both
self-referencing keys are written in one transaction. Credential is a real
MinIO presigned PUT (SIGNED_URL_TTL_SECONDS, lazily read). Permission order:
album invisible 404 -> member 403 -> uploadEnabled 409 -> size/ext 400 ->
category ownership 400. No audit on init (audit anchors on complete).
Pre-step for Task 19: QUEUE_PHOTO_PROCESS contract constant + lazy-safe
BullMQ queue producer (Redis down at boot must not crash the app; enqueue
failure only logs — sweep compensates). e2e reset now also wipes the MinIO
photo-live/dev prefix; jest maps @photo/storage to TS source.
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 22:42:13 +08:00
浅念
39cae26106
feat(api): password login, sliding renewal, revocable sessions
...
- POST /v1/auth/password|refresh|logout per contract operationIds;
long-lived single token (32B hex, DB stores sha256 only), sliding
renewal without rotation, logout sets sessions.revokedAt.
- SessionGuard: Bearer -> session -> user eligibility (disabled/locked/
pending revoke access); all failures map to 401 AUTH_TOKEN_EXPIRED.
- Anti-enumeration: unknown account / wrong password / status-blocked
share one 401 AUTH_INVALID_CREDENTIALS; unknown-account path runs a
dummy argon2 verify to equalize timing.
- Login rate limiting (P1): in-memory account+IP sliding window,
5 failures / 15 min -> 429; success clears. Contract amendment
(registered): new ErrorCodes.AUTH_RATE_LIMITED added to errors.ts +
openapi Error enum + 429 response on /auth/password; generated types
regenerated and committed. P1a single-instance limiter; P5 needs a
Redis-backed implementation.
- Global ValidationPipe (APP_PIPE) enforces class-validator DTOs ->
400 VALIDATION_FAILED via the existing exception filter.
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 21:17:16 +08:00
浅念
368b1d5b47
feat(api): argon2 password, idempotent admin seed, api-exception infra
...
Task 12 + Task 7 quality-review carryovers:
Part A - password hashing & controlled initial admin:
- src/auth/password.ts: hashPassword/verifyPassword over @node-rs/argon2
(napi prebuilt, argon2id default); empty plain rejected at the door
- src/scripts/seed-admin.ts: idempotent admin seed (ADMIN_ACCOUNT/
ADMIN_PASSWORD env, walk-up root .env load); existing account -> skip,
never overwrites the password; no audit_logs (system op, no actor)
- pnpm seed script (build + node dist/scripts/seed-admin.js);
.env.example dev defaults admin/admin-secret-change-me
- tests: test/password.spec.ts (5), test/seed.spec.ts real-DB idempotency (4)
Part B - Task 7 carryovers (all 8):
- ApiException(apiCode, message, httpStatus, details?) recognized first by
the filter; unmapped-status rule: >=500 -> INTERNAL_ERROR + generic
message, <500 -> VALIDATION_FAILED keeping original status (warn-logged)
- 5xx/INTERNAL_ERROR responses forced to generic message (leak fix)
- pino redact adds req.headers.cookie / res.headers.set-cookie
- x-request-id echo capped at 128 chars (fresh UUID beyond, no log)
- LOG_LEVEL validated against pino levels at module wiring -> fail fast
- shared/ constants (API_PREFIX), request-with-id, log-level (+spec)
- jest ts-jest reuses tsconfig.json; unit testMatch includes test/*.spec.ts
- build:watch script (no concurrently dep; dev = build:watch + node --watch)
api: 35 unit + 10 e2e tests green; root lint/typecheck/test green.
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 20:54:30 +08:00
浅念
37f4b3e11f
feat(storage): storage port and minio adapter with forbid-overwrite semantics
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 20:25:18 +08:00
浅念
2801e4f158
feat(api): identity and audit schema with mysql semantics tests
...
packages/db-cn:@photo/db-cn(prisma 5.22,api/worker 共享 client)+
schema(users/sessions/audit_logs,DateTime(6)、cuid、账号唯一)。
迁移 SQL 手工改写表级 collation 为 utf8mb4_0900_bin——Prisma 生成的 MySQL
DDL 固定 utf8mb4_unicode_ci 会覆盖库级 bin collation,账号/token 大小写
敏感语义依赖它(后续迁移需同样复核)。services/api:@Global PrismaModule
(PrismaService 封装 $connect/$disconnect,池上限仍由
DATABASE_URL_CN ?connection_limit=10 承担);reset.ts 落地 FK 包裹式
TRUNCATE;e2e 语义复核 4 例(UTC 往返/大小写敏感/audit Json+actor/
session FK Restrict P2003)。pnpm 10 未拦截 prisma engines(平台包内置),
无需 onlyBuiltDependencies。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 19:51:56 +08:00
浅念
7d250ec342
feat(api): cn-app skeleton, toolchain, env loading convention
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 19:14:41 +08:00
浅念
656498d748
feat(contracts): openapi base, error codes, type generation
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 17:04:12 +08:00
浅念
68a7d6a6d4
chore: pnpm workspace and turbo pipeline
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 16:24:12 +08:00