Commit Graph

98 Commits

Author SHA1 Message Date
浅念 250d4355b9 build(deploy): production images, compose stacks and 1panel guide
P5-prep Task 1(docs/plans/2026-09-21-P5-准备件.md):

- docker/server.Dockerfile:单镜像 photo-live-server 多角色(api-cn/api-ov/
  worker/sync/migrate-deploy/seed-admin,entrypoint.sh 分发;migrate-deploy
  支持 cn|ov|all 区域参数)。三阶段:全量 install(锁文件冻结)→ prisma
  generate 双库 + 逐包构建;--prod 依赖层;runtime=node:20-alpine+openssl/curl
  + 全局 prisma@5.22.0(对齐锁文件)+ 双库 client 镜像内重建(db-cn 落 pnpm
  store .prisma、db-ov 落包内 generated/)。sharp 走 musl 预编译验证通过。
- docker/web.Dockerfile + infra/deploy/nginx.conf.template:photo-live-web
  (nginx:alpine + envsubst 模板)。viewer(/,history 回退) + admin(/admin/,
  hash 路由) + /v1 反代 API_UPSTREAM(含 /v1/realtime WS 升级)+
  /assets/、/admin/assets/、/embed/v1/ immutable 长缓存(admin assets 的
  alias 前缀剥除坑已实测修复)。VITE_* 为构建期参数(VIEWER_BASE/
  EMBED_ORIGIN 随域名构建传入)。
- infra/deploy/docker-compose.cn.yml / -ov.yml:cn=api-cn+worker+sync+web,
  ov=api-ov+web-ov(核实架构:sync 仅 cn 侧,inbox/emergency 内嵌 api-ov,
  无 ov 侧 sync 进程);健康检查 /v1/health、unless-stopped、json-file
  10m×3、外部 1panel-network 复用 1Panel MySQL/Redis、SERVER_IMAGE/
  WEB_IMAGE 环境变量化镜像 tag(回滚杠杆)。
- .env.production.example-cn/-ov:生产变量全量清单(cn 25 键/ov 24 键,
  DB/Redis/OSS/SYNC/AUTH/限速/域名分组注释,源自 services+packages
  process.env 全量审计);.gitignore 放行 example,真实 .env.production-*
  永不入库。
- infra/deploy/README-1Panel.md:镜像导入双路径(save tar 导入 / 仓库 pull)、
  MySQL 建库授权 SQL(utf8mb4_0900_bin,<8.0.30 回退 utf8mb4_bin——D12)、
  Redis 连接、1Panel 编排、OpenResty 反代+证书、migrate-deploy 一次性容器、
  首次部署顺序(DB→迁移→ov 栈→cn 栈→seed→冒烟)。
- 根 package.json pnpm.onlyBuiltDependencies 增补 sharp/prisma/@prisma/
  engines/@prisma/client(镜像构建所需 install 脚本白名单)。

本地验证:两镜像构建成功(server 988MB / web 97.7MB);compose config 校验
通过;server 镜像内逐服务 require(sharp/argon2/双 Prisma client/bullmq/
ioredis)与双引擎 so.node、migrate-deploy 角色、无 .env 泄漏均实测;web 镜像
容器实测路由(viewer 回退/admin 子路径/assets 与 embed immutable/WS 代理路径
502 语义)全通过。未起真实栈(无真实 DB,符合准备件边界)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-21 01:39:55 +08:00
浅念 12059d9a0e docs(plan): p5 preparation plan
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-21 01:00:52 +08:00
浅念 22188ce3ee docs: p4b acceptance record
- P4b 验收记录(A11/A22 简化级/A23 组件/A12+T15 证据、偏差 16 项、
  P5 移交清单、测试统计 web 655 +66 / mobile 280 零漂移)
- 总纲 P4b 进度注记:P1-P4b 全部本地阶段完成,仅余 P5 真实环境接入
  (G5/G6 + D03/D05/D08/D09/D10/D13)
- P4b 计划勾选回填 + 完成头注

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 23:59:21 +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
浅念 0238f505c4 feat(admin): embed slot management with code snippet
P4b Task 5:/system/embed-slots 管理页(Task 8/9 页面模式:useCursorList
游标累积 + 对话框治理)。列表(名称/绑定相册/启停/configVersion/更新时间,
启停开关行内 PATCH → configVersion+1 toast 提示)+ 新建/编辑对话框
(设置表单位置/尺寸/间隔/语言/默认收起,intervalMs 契约区间夹取;编辑按
整体替换语义提交完整 settings)+ 绑定相册对话框(相册标题/ID 搜索选中 →
确认框携带 A22 文案「configVersion 将递增,官网无需改动」;清空即解绑)+
嵌入代码卡({EMBED_ORIGIN}/embed/v1/loader.js + data-slot 令牌,一键复制;
EMBED_ORIGIN=VITE_EMBED_ORIGIN 缺省本机 viewer dev,.env.example 登记)。

- API 层 src/api/embedSlots.ts + types/api/embedSlot.d.ts(契约镜像)
- vitest 9 例:表单→载荷(缺省+夹取)、编辑载荷收敛、snippet 拼接、
  环境基址兜底、绑定变更分类、zh/en 键一致 + A22 短语断言
- Playwright e2e(真实 cn+ov 后端):新建→绑定(gov-photos 直种 +
  seed-fixture 新增 sync-flush 模式经 bootstrap/真实发送管线投影 photos_ov)
  →嵌入代码卡含 token→ov 公开 feed 返回相册照片→换绑 configVersion+1 +
  feed 切换(A22)→停用 feed 404;admin playwright 配置补 ov webServer
- i18n zh/en(embedSlotsManage.* + menus.system.embedSlots)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 23:25:59 +08:00
浅念 8f2c21d25a feat(viewer): embed carousel iframe with visibility pause
P4b Task 4:宿主 loader 以 iframe 载入的轮播页(/embed/:slotToken,裸布局)。

- src/embed/feed.ts:纯函数集——A22 旧响应裁决(dispatch seq + configVersion
  双闸:过期/回看丢弃、等值差分合并、升值全量重置)、照片队列差分(新照片
  追加队尾、缺席淘汰、thumb 签名刷新)、interval/高度夹取、轮询退避;
- src/embed/useEmbedFeed.ts:取数编排(If-None-Match → 304 零处理)、轮询
  (查询 interval 覆盖 → EMBED_POLL_INTERVAL_MS,连续 5 失败 ×5 退避,成功
  归零;404/410 静默 collapse 隐藏自身)、A23 不可见暂停(document 隐藏 +
  IntersectionObserver 滚出视口 + 本地收起;恢复立即校准)、轮播 intervalMs
  切换;__embedPollMs e2e 注入点;
- src/embed/postMessage.ts:上行 resize/collapse(referrer origin 收敛,缺失
  退 '*'——载荷无敏感数据,宿主侧 loader source+origin 双校验)、下行 expand
  (event.source === window.parent 白名单);
- EmbedPage.vue:交叉淡入淡出(仅 thumb 小图省流量)、点击 window.open 单图
  深链(noopener)、计数圆点、「照片即将上线」空态卡、×收起;lang 查询
  (auto→相册 defaultLocale)驱动嵌入词条子集;
- api client:fetchPublicEmbedSlot(304/ETag 分支,不走 apiRequest);
- e2e:宿主夹具页 public/__test__/embed-host.html(仅测试资产)+ embed.spec 7
  用例(轮换/新标签深链/收起记忆/窄屏 sm/轮询增量/换绑重置/空态/幽灵槽);
  helpers 增展示位 admin API 种子 + flushOutbox;
- vitest 18 用例(裁决矩阵/队列差分/夹取/退避/304/可见性状态机)。

配套修正:api CORS exposedHeaders ETag(跨源浏览器可读,304 语义成立);
contracts 新增浏览器安全子路径 ./constants(根出口 re-export internal-sync
的 node:crypto 会打进浏览器包);eslint ignore embed-loader 产物拷贝
(Task 3 起 viewer lint 红,先修)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 23:02:07 +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
浅念 aee4b7ac46 feat(api): embed slots with ov replica and public feed
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 22:22:19 +08:00
浅念 eb51069a12 feat(contracts): embed slots admin and public endpoints
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 21:49:09 +08:00
浅念 e9a7dc8740 docs(plan): p4b review fixes (etag recipe, sandbox origin, narrow screen)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 21:41:19 +08:00
浅念 ec7837ebb0 docs(plan): p4b implementation plan (pending review)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 21:38:37 +08:00
浅念 17d09bbb01 docs: p4a acceptance record
P4a Task 10 收口:回归门全绿(web lint/typecheck/build 9/9 包任务,
单测 271 + e2e 318 = 589;mobile analyze 0 issue + 280/280 零漂移)。
验收记录含 A16/A17/A18/A20/A23/A29 证据、D06 工作默认登记、契约
增补汇总(36→51 operations + DOWNLOAD_DISABLED)、偏差 14 项、
G4 待真实接口清单(UserSourcePort 替换点 + D08 字段确认)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 21:21:08 +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
浅念 618d1d796c feat(admin): account governance and user sync pages
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 20:26:53 +08:00
浅念 3681999667 feat(api): scheduled user sync with mock source (T30 semantics)
P4a Task 7 —— cn 侧用户定期同步(mock 源先行,G4 真实接口到位后仅替换
UserSourcePort provider,引擎/契约/表不变)。

数据模型(db-cn 迁移 20260920114149,utf8mb4_0900_bin 逐表校对):
- user_sync_runs:批次账本(trigger cron|manual、state running|success|
  partial|failed、cursor 水位、counts JSON、起止时间与错误摘要);
- user_sync_errors:单批次坏行明细(runId 索引);
- external_user_mappings:本地↔外部 ID 映射(@@unique([sourceSystem,
  externalUserId]),sourceVersion 逐行高水位);
- user_sync_state:单行状态(lastRunAt/lastSuccessAt/cursor/runCount)。

同步引擎(UserSyncEngine):
- Redis SETNX 分布式互斥(TTL 10min,锁值=runId,Lua CAS 释放);
  定时 tick 与手动触发共用,运行中批次复用 runId(202);
- 游标增量 + 周期全量对账(RECONCILE_EVERY_N_RUNS 默认 10);
- 逐页单事务提交(run counts+cursor / state cursor),分页中断→failed
  且水位不前进、会话不动;
- 红线遵守:sync 不写 password_hash/role/album_members/localLocked;
  空/部分页不误伤停用;上游恢复不解除本地锁定;
- 新用户按 pending 建(随机不可用哨兵哈希,激活+重置后方可登录);
- 停用即撤销该用户全部会话;本地预建同名账号走关联而非重建。

Admin API(/admin/user-sync):GET status|runs|errors?runId=、POST runs
(202 {runId});runs 为 startedAt+ID 倒序游标分页。

auth.service:登录对哨兵/损坏哈希加固(等代价 dummy verify,
401 而非 500,时序平坦)。

测试:user-sync.e2e-spec 12 用例(首轮全量/改名幂等/停用撤会话/
恢复不解锁/新增/空页/分页中断/部分无效/手动互斥/增量+对账/API 形状
与权限/pending 登录门);api e2e 22 套件 239 用例全绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 20:01:17 +08:00
浅念 f4a1ba7a14 feat(api): admin account governance endpoints
P4a Task 6: GET /admin/users 双模式收口(无 accountPrefix 进入管理列表
模式:全量用户、query/status 过滤、createdAt+ID 倒序键集游标、仅 admin;
前缀模式 P1b 语义保留)+ PATCH / reset-password / activate / deactivate
治理端点(sourceStatus 只读拒绝、自我保护、会话撤销、审计、生成初始
密码)。附带修复 full-chain e2e 的 job-done 断言竞态(轮询条件补齐
photo_jobs 终态)。api e2e 214 → 227 全绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 19:31:00 +08:00
浅念 4937eeecff feat(api): usage summary with threshold alerts
P4a Task 5(A23 后台侧):用量统计与阈值告警后端全链。

- cn 迁移(校对手决 utf8mb4_0900_bin):usage_thresholds 单行配置表
  (迁移内种子行,storageBytes/perAlbumBytes BigInt?,null=维度停用)+
  usage_alerts 告警账本(kind storage|perAlbum、albumId?、resolvedAt?
  索引)。「同 kind+albumId 活跃告警唯一」为应用级约束(MySQL 无部分
  唯一索引):评估事务以单行配置 FOR UPDATE 作串行化锚点闭合竞态。
- 共享核心 packages/db-cn/src/usage.ts:computeUsageAggregates(非
  deleted 口径实时 SUM:photos groupBy + 变体 JOIN raw)+ evaluateUsage
  (storage/perAlbum 两维超阈 ensure、回落/停用 resolve、去重)+
  evaluateUsageSafe(自开事务、失败仅记日志不拖垮业务主流程)。api 与
  worker 共用(放置原因见文件头)。
- api:GET /admin/usage/summary(相册聚合 + totals + 阈值 + 告警:活跃
  在前、近期解除补位,合计 100)与 PUT /admin/usage/thresholds(缺省
  保持/显式 null 清除 + 契约「即刻重新评估」);仅 admin。BigInt 运输
  决策:库内 BIGINT、JSON 出入口 Number() 收敛(2^53≈9PB 上限,DTO 以
  MAX_SAFE_INTEGER 拒绝超界输入杜绝静默失真)。
- 检查点(提交后评估):worker 发布事务后(仅真实 published)、worker
  水印任务完成后、api photos 可见性迁移(PATCH/软删除)与批量隐藏后、
  阈值 PUT 后。
- tests:usage e2e +6(汇总数学/降序/标题、403/401、400 校验矩阵、
  perAlbum 告警产生-去重-上调解除、storage 告警-null 清除解除、hidden
  计入+deleted 回落且穿过 api 检查点);两侧 reset 增补 usage 表截断 +
  单行重种。全量回归:api e2e 214/214、api 单测 51/51、worker 单测
  14/14 + e2e 13/13。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 19:01:41 +08:00
浅念 2bb6f291d3 feat(ov,viewer): two-tier download with wechat guidance
P4a Task 4(T12 收口):ov 域公开照片下载端点 + viewer 下载 UI。

- interactions 增 downloads:GET /public/albums/{token}/photos/{photoId}
  /download?variant=browse|original。两级开关(A18):browse 需
  downloadEnabled;original 需叠加 originalDownloadEnabled(级联,browse 关
  则两者均 403 DOWNLOAD_DISABLED)。水印相册 browse 现签 watermark pattern
  key(browse-key 解析抽取 shared/ov-browse-key 与 public-ov 复用);原图
  现签 originalObjectKey(SIGNED_URL_TTL)。来源 IP 固定窗限速
  (DOWNLOAD_WINDOW_REQUESTS/DOWNLOAD_WINDOW_SECONDS,env 可调);零浏览
  计数写入(排除规则)。STORAGE_PORT_OV 经新 StorageOvModule 与 public-ov
  共享同一实例。
- 契约微调登记:PublicAlbum 增 readOnly 必返 downloadEnabled/
  originalDownloadEnabled(cn+ov 同形返回;public e2e 精确键断言同步)。
- viewer:lightbox 下载入口随开关渲染(无/单按钮/分体菜单两选项)、过期
  凭证静默重取一次、微信内置浏览器(MicroMessenger UA)出保存图片指引
  浮层(复制深链 + 反馈)替代直接下载;i18n zh/en 词条。
- tests:api e2e +7(开关矩阵 + URL/字节级断言、水印键一致、隐藏 404、
  限速 429、零计数行);viewer vitest +9(UA/矩阵/菜单/重取/失败/指引/
  复制);viewer Playwright +3(入口矩阵、微信 UA 浮层、真实端点 200 +
  签名 URL 模式)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 18:32:34 +08:00
浅念 243b2f8721 feat(worker): watermark versioning with atomic history regeneration
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 17:53:44 +08:00
浅念 6723a0ce88 docs(plan): p4a implementation plan (approved)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 16:39:03 +08:00
浅念 a125fcab1a feat(api): batch hide with per-item results and owner transfer
P4a Task 2(A16/A17):

- POST /albums/{albumId}/photos/hide-batch:逐项结果(部分成功)——
  不存在/他册 → ok:false PHOTO_NOT_FOUND;非 published(unpublished/
  deleted)→ ok:false PHOTO_NOT_PUBLIC;已 hidden → ok:true 幂等 no-op
  (同值不产生第二次事件/审计/版本,P1 语义);published → 逐照片事务
  (条件更新 + outbox photo.hidden 高优先级 + audit photo.hide)。
  results 与请求同序;权限同管理照片列表(仅创建者或管理员,404 防枚举);
  单批 1..200(DTO 层 400)。
- POST /admin/albums/{albumId}/transfer:admin-only owner 移交——事务内
  条件更新 ownerId+entityVersion+1(乐观重试)+ 成员互换(目标 upsert 为
  owner,原 owner 按 keepAsCollaborator 降级 collaborator 或删行)+
  outbox album.updated(标准载荷,版本推进)+ audit album.transfer
  {from,to,kept}。目标校验同成员邀请资格谓词(active/未锁定/未停用);
  自交接 400;photos.uploaderId 不动(A17 历史上传人不变)。
- e2e(batch-ops.e2e-spec,双入口 harness):8 用例覆盖逐项结果/幂等/
  入参权限矩阵/ov 传播(真实发布链→隐藏→photos_ov=hidden+公开列表剔除)/
  keep 两分支/目标校验/交接后权限(新 owner 管理、旧 owner 保留协作者
  可上传)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 16:36:34 +08:00
浅念 8e3e19a7d3 test(api): fix key-order expectation in users-lookup e2e
Object.keys().sort() 是码点序(大小写敏感):'localLocked'(大写 L)排
在 'locale' 之前;原断言字面量按不区分大小写的字母序书写导致确定性失败
(P4a Task 2 全量回归时暴露,与 HEAD 行为无关的既有笔误)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 16:36:19 +08:00
浅念 8a8ad8a18d feat(contracts): p4a admin governance, watermark, download and usage endpoints
P4a Task 1 契约扩展批次(36 → 51 operations,gen/redocly/枚举同步守护绿):

- photos:POST /albums/{albumId}/photos/hide-batch(photoIds 1..200,逐项
  BatchHideResult {photoId, ok, code?},部分成功语义,A16);
- admin:POST /admin/albums/{albumId}/transfer(owner 交接,历史上传人不动,
  keepAsCollaborator,A17);
- albums:POST/GET /albums/{albumId}/watermark-jobs(WatermarkConfig 版本化
  + 历史重生成任务 state/progress/total/failed,原子切换、原图永不覆盖,
  A18/A20);
- interactions:GET /public/albums/{token}/photos/{photoId}/download
  (variant=browse|original 两级开关校验;**新增错误码 DOWNLOAD_DISABLED**,
  errors.ts 与 Error enum 两侧同步,A18);
- admin 账号治理:GET /admin/users 统一为 listAdminUsers 双模式 + PATCH
  /admin/users/{userId} 与 reset-password / activate / deactivate;
- admin 用户同步:GET /admin/user-sync/{status,runs,errors} + POST runs
  手动补拉(互斥复用进行中批次,A29);
- admin 用量:GET /admin/usage/summary + PUT thresholds(null 清除、
  即刻重评估,A23)。

偏差登记①——/admin/users 路径冲突(P1b lookupUsersByAccount):P4a 决策
统一为双模式端点(前缀模式沿用 P1b 语义与任何已认证工作人员可用;管理
列表模式 query/status 过滤 + 游标 + 仅 admin),响应统一 AdminUserList
(AdminUser 字段超集,游标元信息随行)。消费方同步:admin 邀请搜索
(fetchLookupUsers/类型镜像,行为不变)与 api 过渡实现(仍仅前缀模式、
limit 对齐 1..100 缺省 20;管理列表模式与仅 admin 判定随 Task 6 落地,
users-lookup e2e 已按统一形状修订)。

偏差登记②——apps/mobile 手写镜像未随本次更新:其邀请流程仍按旧 3 字段
投影消费(AdminUserList 为超集,运行时兼容,mobile IT 不经该路径);列
为 P4a 跟进项,仅当其 e2e 破坏时处理。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 15:56:38 +08:00
浅念 45d46518b1 docs(plan): p3 completion header note
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 15:23:45 +08:00
浅念 e484aa5427 docs: p3 acceptance record
P3 收口(Task 12):回归门全绿实测数字、MuMu 设备集成证据(阶段 1
十二腿 + 阶段 2 双场景,引用 it-logs 存档原文)、29+3 轮排障 13 根因
展开表、契约增补(password-change + sourceDeviceId)登记、G2 真机
清单(计划全量 + 集成期新增 5 项)、G3 引用 README-ios、偏差表(计划
内 13 + 计划外 5);总纲 P3 行进度注记(G2 待用户真机执行、D03 待确
认);P3 计划 Task 11/12 勾选。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 15:21:49 +08:00
浅念 d41be367f8 fix(mobile,it): integration suite stabilization on real device
Root causes found & fixed across 29 phase-1 + 3 phase-2 runs:
- MSYS path mangling for adb shell device paths (per-command MSYS_NO_PATHCONV + winpath)
- adb reverse tcp:9000: presign URLs embed server-side localhost MinIO host,
  unreachable from emulator (uploads hung forever)
- compose --profile ov omitted: Docker restarts stranded ov side (ov-api 500s)
- soft keyboard occlusion on dialog buttons (dismissKeyboard + ensureVisible)
- stacked-route BackButton ambiguity (goBack must tap topmost hit-testable)
- bottom-sheet entrance animation race (wait for hit-testable option)
- language leg: drive LocaleSettings provider (sheet tap flaky, covered by unit tests)
- password page: self-pops on success (no extra goBack)
- bg verify: server-side authoritative assertion (fresh process = fresh memory DB)
- flutter test uninstall-reinstall wipes app data: image dir fallback to /data/local/tmp
- run-as cp failures no longer silently swallowed
- phase-2 missing IT_STAFF_ACCOUNT define

Phase 1: all 12 legs green (login→album→category→invite→3 uploads via native
WorkManager→server history→ov visible→hide sync→network fault/recovery→
password change→relogin→locale switch)
Phase 2: bg-home + kill-restart green (A04/A05 emulator-level)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 15:03:17 +08:00
浅念 fb3c278e50 test(mobile): integration suite against real backend on mumu
P3 Task 11 (WRITE & COMPILE phase; device run pending MuMu availability).

- integration_test/p3_flow_test.dart: login -> album (zh+en) -> category
  -> invite -> enqueue 3 real JPEGs -> queue completed -> server history
  (sourceDeviceId) -> ov public API sees 3 -> hide via API + UI sync ->
  DIO fault-gate offline (waitingNetwork -> retry -> 4 completed) ->
  password change (session kept) -> relogin -> in-app locale smoke.
- integration_test/p3_background_test.dart: IT_PHASE=prepare/verify spec
  for HOME-background continuation and force-stop kill-restart recovery
  (2 x ~14MB noise JPEGs give a deterministic mid-flight window).
- helpers: dart-define config, fault-injecting DIO adapter, app harness
  (picker/notification/locale provider overrides), UI step library with
  live-binding polling (no pumpAndSettle).
- scripts/run-mobile-it.sh: device detect (exit 2), backend ensure
  (compose + api/ov/worker/sync, exit 3), idempotent per-run user seed
  (no DB reset), APK build/install + adb push of testdata, phase 1 /
  --background phase 2 orchestration with server-side confirmed-count
  truth via curl.
- services/api seed:it script (create-if-absent staff accounts).
- debug-only network security config: cleartext HTTP permitted to
  10.0.2.2/127.0.0.1/localhost only (release unaffected).
- testdata: 3 x ~120KB + 2 x ~14MB generated JPEGs.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 05:35:16 +08:00
浅念 8f3966d7b5 feat(mobile): in-app locale switching
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 05:00:11 +08:00
浅念 955f9b13c1 feat(mobile): upload queue ui and continuous capture page
Task 9 (P3 plan): queue tab replaces the stub, continuous capture page
enqueues per shot, album detail gains an upload FAB, and the upload
engine lifecycle is now bound to the login session.

- Engine scope: session account drives UploadEngine start/stop; wired
  from app boot, login success, logout, and 401 eviction. Tasks stay in
  the store across logout (accountId hard isolation).
- Queue tab: album x state x time filter bar, task rows with thumbnail,
  state chip, localized error copy, attempt count, per-row progress via
  a broadcast progress hub, state-dependent actions (re-pick retry,
  retryNow, cancel, delete), batch retry bar, 200ms-debounced live
  refresh from drift table updates.
- Capture page: camera preview via injected CaptureSource, one enqueue
  per shutter press, persistent album/category/badges overlay with a
  switch-only-affects-new-shots hint, gallery multi-pick, exit confirm
  while uploads are pending, permission-denied and init-failure views.
- POST_NOTIFICATIONS runtime permission via a small MethodChannel
  (photolive/permissions) + PermissionPlugin.kt: single runtime
  permission need, avoids permission_handler dependency; requested once
  on first enqueue, silent degradation when denied or unavailable.
- Album detail FAB: capture-and-upload / pick-from-gallery sheet.
- l10n: ~50 new keys in zh/en arbs; tests: +45 (filter matrix, engine
  scope lifecycle, permission gate, queue tab, capture page, FAB entry);
  existing page tests updated to inject session/task-store fakes.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 04:47:31 +08:00
浅念 42e1c2e34b feat(mobile): ios background upload bridge (compile-gated at G3)
Swift 侧(协议与 Android UploadPlugin.kt 逐字一致,编译/真机留 G3):
- UploadBridge.swift:通道/方法/事件/失败码常量 + registerTask 参数解析
  (镜像 UploadProtocol.kt)
- UploadEventBus.swift:单调 seq + 终态 ack 缓存重放(镜像
  UploadEventBus.kt),iOS 增强——未 ack 终态落盘(系统后台拉起投递的
  回调在进程重启后同样重放,技术方案 §6.2 持久化原生回调)
- UploadTaskStore.swift:taskId→URLSession taskIdentifier 映射 plist 持久化
  (Application Support;对应 Android WorkManager 自持映射)
- UploadPlugin.swift:URLSession background
  (com.photolive.photo_live.uploads)+ delegate 失败分类(2xx→completed+etag
  / 403→needsRenew / 4xx→HTTP_4XX / 网络→重试上限后 NETWORK,镜像
  UploadPutExecutor/UploadWorker)、原生重试(5 次上限、10s 指数退避)、
  进度 500ms 节流、queryTask/cancelTask/ackEvents、静默取消
- AppDelegate.swift:implicit engine 通道注册 + 启动重建会话 +
  handleEventsForBackgroundURLSession completionHandler 暂存
- Info.plist:UIBackgroundModes 有意不声明(background URLSession 无需;
  计划措辞按技术方案 §6.2 修正,见 README-ios.md)
- project.pbxproj 登记新 Swift 文件;README-ios.md(Xcode 步骤 + G3 验证
  清单:实际文件上传/持久化回调窗口/服务端补偿/重启重联校准/强制退出/
  低电量延迟/100 张批量)

Dart:NativeTransport 选择矩阵并入 iOS(Platform.isAndroid || isIOS,
FORCE_DART_TRANSPORT 覆盖不变)——协议两侧一致,无新通道代码。

flutter analyze 0 issue;flutter test 219 全绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 03:42:49 +08:00
浅念 4e35bdc0b2 feat(mobile): android background upload bridge via workmanager
Kotlin 侧(MethodChannel photolive/uploads + EventChannel photolive/uploads/events):
- UploadPlugin:registerTask 入队 WorkManager OneTimeWorkRequest(unique
  work=upload-task-{taskId},expedited + OutOfQuotaPolicy 回退、CONNECTED
  约束、指数退避 10s 起)、cancelTask / queryTask / ackEvents
- UploadWorker(CoroutineWorker):dataSync 前台服务 + 进度通知,
  OkHttp PUT(ETag 捕获、64KB 分片进度),结局分类:2xx→completed、
  403→needsRenew、4xx→failed{HTTP_4XX}、网络/5xx→Result.retry(上限
  5 次后 failed{NETWORK|SERVER_ERROR})、缺文件→failed{SOURCE_MISSING}
- UploadEventBus:终态事件单调 seq + 未 ack 缓存重放(上限 200),
  重注册/取消作废旧缓存——进程重启/切后台恢复不丢完成信号
- Manifest:FOREGROUND_SERVICE(_DATA_SYNC)/POST_NOTIFICATIONS 权限 +
  SystemForegroundService tools:node=merge 补 dataSync 类型(已验证
  合并产物)

Dart 侧:Transport 接口(init/complete/renew 留引擎,仅 PUT 委托)——
DartHttpTransport(Task 6 逻辑抽出,403→renew 异常归一)+ NativeTransport
(registerTask→事件流终态/进度映射、终态 ack、取消联动);引擎传输选择:
Android 缺省原生,dart-define FORCE_DART_TRANSPORT=true 强制 Dart 路径;
失败分类与 dio 路径语义对齐(401 短退避/429 网络桶/SOURCE_MISSING 不
自动重试等)

测试:gradle testDebugUnitTest 29 例(executor MockWebServer 分类×10、
协议解析×11、事件总线×8);flutter test 219 全绿(新增 NativeTransport
事件映射/取消/ack/并发路由、引擎×Transport 委托与 needsRenew 重注册、
传输选择矩阵);flutter analyze 0;debug apk 构建通过

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 03:22:42 +08:00
浅念 3ddb0ea2d4 feat(mobile): upload engine with preparation, batching states and retries
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 02:50:40 +08:00
浅念 d96e56f653 feat(mobile): drift upload task store with state machine
- upload_tasks 表(accountId 隔离 + 三个复合索引)、状态列枚举转换器、
  datetime 文本落库(UTC 归一,亚秒精度)
- 纯状态机(task_state.dart):合法迁移表 + canTransition/assertTransition,
  completed/cancelled 终态
- TaskStore 仓储:create(clientTaskId 同账号同相册应用侧唯一)、
  updateState(迁移守护+旁路字段)、账号隔离读路径(硬守护)、
  countsByState 徽章、cleanupCandidates/markCleanable(仅终态可清理——
  评审缺口 1 硬守护,未完成源文件永不清理)
- 测试 26 例(状态机表驱动全边 + 守护/隔离/筛选/保留策略);
  sqlite3 3.x build hooks 在 flutter test 宿主与 Android APK 构建均验证
  (libsqlite3.so 已打包全 ABI,运行时验证留 Task 11)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 02:12:39 +08:00
浅念 bbea1dd364 feat(mobile): album management pages with qr share
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 01:56:38 +08:00
浅念 8be21b856b feat(mobile): login, password change and logout screens
P3 Task 3 移动端(对接 Task 2 api client + 本任务新增的后端契约):

- 登录页实装:账号/密码表单(空值客户端校验)→ api.login(会话自动
  持久化)→ pushReplacement /home;错误码本地化(AUTH_INVALID_
  CREDENTIALS / AUTH_RATE_LIMITED(含稍后再试文案)/ 网络 / 校验);
  提交加载态防重复提交。
- 修改密码页(设置页入口):当前/新/确认三字段,客户端校验(≥8 位、
  两次一致)→ api.changePassword → toast + pop;成功后本地会话不变
  (服务端保留当前会话);旧密码错误(401 AUTH_INVALID_CREDENTIALS)
  显示「当前密码错误」且不触发 401 驱逐。
- 退出登录:设置页按钮 → api.logout 尽力而为 → 清会话 → 清栈回
  /login;App 壳 initState 注册 onUnauthorized(业务 401 → 清栈 /login),
  根 navigatorKey 接线;password-change 的 401 豁免会话驱逐(拦截器
  豁免集合与令牌附加分离)。
- l10n:新增 16 组 zh/en 文案;错误码 → 文案解析扩展
  (core/error_l10n.dart,errorCode→AppLocalizations)。
- 测试:ApiClient.changePassword 单测 ×2(Bearer+请求体;旧密码 401
  不驱逐);widget 测试 ×12(登录 5:空值/成功跳转/凭据错误/限速文案/
  加载禁用;改密 4:过短/不一致/旧密码错误文案+会话保留/成功 toast+pop
  且令牌不变;设置 2:改密入口跳转/登出清会话回登录;App 壳 1:401
  清栈回 /login)。flutter analyze 0 issue;flutter test 81/81。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 01:19:57 +08:00
浅念 31ba03de11 feat(api): password change endpoint and source device id on uploads
P3 Task 3 契约增补批次(单一事实来源 openapi.yaml + cn 实现 + e2e):

- POST /v1/auth/password-change(operationId changePassword,需求 4.1 的
  P1 契约缺口补齐):Bearer + {oldPassword 1-128, newPassword 8-128} → 204;
  旧密码错误 401 AUTH_INVALID_CREDENTIALS;成功后事务内更新 argon2 哈希、
  撤销该用户除当前会话外的全部其他会话(当前会话保留,本人不下线)、写
  审计 auth.password_change。
- sourceDeviceId(A19 设备来源语义):UploadInitRequest 可选 8-64 位设备
  标识;upload_sessions 落列(VARCHAR(64) utf8mb4_0900_bin,可空,存量
  行 NULL 兼容);GET /uploads 条目回显;幂等重放不比对(first-writer,
  首次落库值不随重放漂移)。
- e2e:auth 增 4 例(未认证 401 / 新密码过短 400 / 旧密码错误 401 且会话
  无副作用 / 成功后他端会话撤销当前保留 + 新旧密码登录裁决 + 审计);
  uploads 增 1 例(携带落库回显、换设备重放保留首值、缺省 null、长度
  越界 400)。api e2e 179 → 184;contracts gen/redocly/单测、worker、
  sync 门全绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 00:59:39 +08:00
浅念 f975321b67 feat(mobile): api client, session storage and contract-aligned models
P3 计划 Task 2(apps/mobile):

- core/config.dart:API_BASE/VIEWER_BASE 经 --dart-define 覆盖,缺省
  10.0.2.2 模拟器宿主回环;viewer 相册链接构造 {VIEWER_BASE}/a/{token}。
- core/error_codes.dart:errors.ts 17 码 1:1 镜像(含注释),另设 4 个
  客户端本地伪码(NETWORK_ERROR/NETWORK_TIMEOUT/CANCELLED/HTTP_ERROR)。
- core/api_client.dart:dio 封装——Bearer 注入(登录端点豁免)、401
  驱逐(清会话 + onUnauthorized 回调去重,App 壳注册跳 /login)、
  DioException/错误体 {code,message,requestId} 归一 AppException、
  通用游标分页助手;auth/albums/members/categories/admin/uploads/photos
  全端点方法;riverpod provider 出口。
- core/session_store.dart:SessionStore 抽象 + flutter_secure_storage
  实现(token/expiresAt/account 三键,encode/decode 纯函数可测)。
- features/{auth,albums,uploads,photos}/models.dart:手写 fromJson/
  toJson,逐字段对齐 openapi.yaml(类注释标注契约 schema/operationId);
  locale 按字符串透传(zh-CN 标识符不兼容),业务枚举 Dart 化并在
  未知值 fail-fast。
- 测试 66 项:17 码表驱动 + 网络/超时/取消 + 兜底映射、游标边界
  (含 CursorPageMeta 不变式违约容错)、模型往返(fixture 转写自
  openapi.yaml schema 与 api e2e 断言,来源在测试头注释)、拦截器
  行为(手写 FakeAdapter:Bearer 注入/401 驱逐/登录豁免/并发去重/
  尽力而为 logout/refresh 滑动续期)。flutter analyze 零告警。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 00:40:02 +08:00
浅念 8bc50e07ef feat(mobile): flutter scaffold with env script and baseline nav
- scripts/flutter-env.sh: toolchain PATH/JAVA_HOME/ANDROID_HOME + pub 镜像 env
- apps/mobile: flutter create (com.photolive.photo_live, android+ios)
  - Android minSdk 24 / targetSdk 36;INTERNET 权限(main manifest,debug+release)
  - iOS Info.plist:本地网络 ATS 例外 + 相册/相机用途说明(zh+en)
- 依赖接入:riverpod / drift(+sqlite3_flutter_libs) / dio / secure_storage /
  image_picker / camera / qr_flutter / path_provider / path / intl
- l10n:l10n.yaml + app_zh/app_en arb(20 键),gen-l10n(zh 模板,非空 getter)
- 基线:Material 3 主题、/login→/home 命名路由、底部导航(相册/队列/设置桩)、
  locale provider 脚手架(Task 10 完成切换)
- flutter analyze 零问题;widget 测试通过;debug apk 构建成功
  (MuMu 冒烟因宿主 commit 内存耗尽未完成,Task 2 重试)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 00:22:04 +08:00
浅念 5879552ecb docs(plan): p3 flutter app plan (approved; review fixes applied)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 23:30:58 +08:00
浅念 79eb975096 docs: p2 acceptance record
P2 Task 12: 验收记录(A09/A10/A14/A21 部分/A26/A27 仿真级 + A25 部分
本地直读证据;D07/D11 初值确认——30min 浏览窗口、2s 推送/5min 对账/
60s 回传建议维持待 P5 实测冻结;13 项偏差登记;P4a/P5 移交清单;回归
门实测统计——全仓 448 测试绿)+ 总纲 P2 行进度注记。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 20:17:54 +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
浅念 cdead49838 feat(viewer): likes, bubbles, visit reporting and hot sorting
Task 10 viewer 互动化:

- 点赞:useLikes 批次状态机(500ms 防抖 / 5 次满额即冲 / 失败同 batchId
  退避重试 3 次后回滚丢弃),乐观计数 + 服务端 total 权威校准
  (countVersion 单调),大图与格子双入口;
- WS 实时:RealtimeClient 纯状态机(半频心跳、指数退避重连、三败降级、
  重连成功 resync 第 1 页重取),消息路由器做 likes.burst 自身过滤
  (originBatchIds 求交)、隐藏页暂停与减少动画下仅校准不出气泡;
  photo.hidden/deleted 即时移出序列并回退邻居大图;降级后 8s 轮询接管;
- 浏览上报:visitorId 跨标签 BroadcastChannel first-writer-wins 协调,
  可见时上报、requestId 持久幂等、网络失败指数重试;
- 排序:最新/热门 chips(热门走 ov 冷快照),大图打开期间禁用切换;
  顶栏「减少动画」偏好开关;
- 契约包新增 ./realtime 子路径出口(浏览器面不可引桶出口——其连带
  internal-sync 的 node:crypto 会被 Vite 外置化致整页崩溃);
- e2e 注入钩子 __viewerWsUrl/__viewerWsRetryBaseMs/__viewerRealtimePollMs/
  __viewerBubbleMs 与 .activity[data-connection] 连接态锚。

vitest:viewer 105(新增 36:likes 批次 8、visitor 协调 5、上报门控 4、
路由 8、WS 状态机 7、排序 4);Playwright:viewer 21(新增 likes.spec 7、
journey 补互动腿断言)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 19:52:27 +08:00
浅念 955bea49b5 feat(sync): stat snapshot return path with version replacement
P2 Task 9 统计回传(ov→cn):
- cn 迁移:albums 增 viewCountReplica/statsVersion/statsSyncedAt、
  photos 增 statsVersion(like_count 已有副本位;无 VARCHAR 新列不涉校对手决)
- cn 接收端 POST /v1/internal/sync/stat-snapshots(InternalSyncGuard):
  整批单事务,逐快照 updateMany WHERE statsVersion < statVersion —— 仅更高
  statVersion 以权威 total 整体替换副本(绝不相加),重复/乱序幂等忽略;
  accepted 只数实际应用条数;views 应用同时推进 statsSyncedAt(A27 展示)
- ov 回传发送器 services/sync/stat-return.ts:聚合 outbox_stat_events 未发送
  行按(kind, photoId|albumId)取 MAX(statVersion) 折叠突发 → HMAC 推送 →
  200 后按计数器标记截至该版本全部源行 sentAt;失败留待下轮重发;main.ts
  第三循环 STAT_RETURN_INTERVAL_MS(默认 60s,env 可调)
- 契约 AlbumSummary + statsSyncedAt(string|null readOnly,+gen);listAlbums/
  getAlbum 经 toSummaryResponse 暴露(管理 UI 列留 P4a)
- e2e(services/sync stat-return.e2e-spec,7 例):逐次点赞回传替换式收敛
  (2→5→10 非累加)、浏览窗口去重回传 + statsSyncedAt、重复回传 accepted=0
  值不动(A27 不重复累计)、乱序高版本替换(total 可下调)+ 低版本忽略、
  发送端折叠只发最高版本且被折叠行随批确认、管理列表 likeCount/statsSyncedAt
  展示、新端点 401/400 守卫

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 18:45:43 +08:00
浅念 1f2a0c658f feat(ov): windowed dedup visit counting with retention cleanup
Task 8 (P2): POST /public/albums/{token}/visits with visit_sessions
window dedup (VISIT_WINDOW_MINUTES), requestId idempotent replay
(echoes stored outcome; expired beyond VISIT_REQUEST_TTL_MINUTES
rejected), album_stats viewCount/statVersion authority + outbox_stat_events
views snapshots, WS stats.updated broadcast, visitorId+IP fixed-window
rate limit (VISIT_WINDOW_REQUESTS), and ov CleanupModule scheduler
(setInterval, CLEANUP_INTERVAL_MINUTES) deleting like_batches /
visit_requests / visit_sessions past their windows — totals never
touched. Contract prose aligned to replay semantics; constants
VISIT_WINDOW_REQUESTS / CLEANUP_INTERVAL_MINUTES added.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 18:14:01 +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
浅念 0fcf18cc5c feat(ov): public read api on replicas with hot sorting
P2 Task 6(ov 公开读 API + viewer 切换):
- ov 装配 PublicOvModule(仅 app-ov.module):/public/albums/{token}、
  /photos、/photos/{id} 三端点同契约读 ov 副本——translations/categories
  自 JSON 列解析(防御性过滤),banner 经 ov 侧 STORAGE_PORT_OV 预签名
  (同本地 MinIO;只读属约定,真实 OSS 只读凭证 P5),viewCount 取
  album_stats 权威累计(无行默认 0;计数端点 Task 8 接线)
- 排序:latest=发布时间 keyset(shared/cursor.ts 同 cn);hot=likeTotal
  desc + photoId desc 决胜,顺序按 `${albumId}|${category}` 短期快照
  (内存 TTL 默认 30s,HOT_SORT_TTL_MS 惰性读取)——计数逐请求现读、
  顺序冻结窗口内一致,偏移游标(base64url 整数)在快照内严格稳定;
  非法 sort 400 VALIDATION_FAILED(cn 不感知该参数)
- 契约:viewCount 放宽为 integer|null(cn null、ov 权威值);
  listPublicPhotos 增 sort 枚举(latest|hot,缺省 latest);
  PhotoPublishedPayload 增 categoryId——补上 Tasks 3/4 管线缺口
  (payloads/publish.service/sync-inbox 投影 + 关联测试随改)
- e2e(services/api/test/public-ov.e2e-spec.ts,7 用例):双入口同进程
  起 cn+ov,cn API 种子→真实发送管线投递;断言相册/列表/单图与 cn
  深等(仅签名 URL 剔除)、banner 真字节、参数矩阵 400/404、hot 排序
  TTL 内快照(计数新/顺序旧)+TTL 过期翻转+偏移翻页、隐藏/下线/紧急
  停用逐态撤下
- viewer 切 ov:API 基址 VITE_API_BASE(缺省 http://127.0.0.1:8081,
  空串=同源网关;Vite 只读 apps/viewer/.env*,根 .env 对前端无效),
  移除 /v1 dev 代理(CORS_ORIGINS 增 127.0.0.1:5175 直连两形);
  playwright webServer 增 dev:ov;e2e 夹具 flushSync(albumId)=cn
  bootstrap 快照 + deliverPendingOnce(sender 新导出)经真实管线投影
  种子,deleteSeededPhoto 改软删让墓碑可同步;14 个 Playwright 全绿
  (journey 管理/staffB 腿仍走 cn 8080)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 17:05:56 +08:00
浅念 eac8da6d6c feat(ov): emergency disable channel independent of sync
P2 Task 5(海外紧急停用入口):
- ov 新增 POST /v1/internal/emergency/disable|enable(InternalSyncGuard
  HMAC,albumToken/albumId 恰好其一、都缺省 400;副本不存在 404
  ALBUM_NOT_FOUND):直写 albums_ov.emergencyDisabled(唯一 ov 本地可写
  位),独立于同步版本链——更高版本 album.updated 重放不解除(e2e 断言
  entityVersion 推进而标志保持),恢复仅经显式 enable;幂等语义
  (alreadyDisabled/wasDisabled 入审计行)
- cn 侧不感知:无对应 cn 状态位、无反向同步(契约描述文档化)
- 审计:pino child(audit=emergency 字段常驻每行)结构化日志——偏差
  登记:计划原文「写 ov 侧日志文件」落地为字段标记而非独立文件通道
  (免新增文件 transport 依赖,P5 运维按字段路由聚合);注入令牌
  EMERGENCY_AUDIT_LOGGER,测试覆写为内存目的地断言字段
- isAlbumVisibleOv 共享判定(status=active && publicEnabled &&
  !emergencyDisabled——镜像 cn 公开语义叠加本地停用位):Task 6 公开
  API / Task 7 WS subscribe 的 404 ALBUM_NOT_PUBLIC 语义单一实现,
  判定矩阵单测锁定
- 契约 [internal] 增 emergency disable/enable 操作与 schema(gen 已跑、
  redocly 通过);e2e:token/id 定位、幂等、enable 恢复、未签名 401、
  均缺省 400、未知相册 404、同步事件不解除

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 16:19:02 +08:00
浅念 36082c5a74 feat(sync): reconciliation scheduler with digest and bootstrap healing
P2 Task 4(inbox 对账):
- services/sync 新增 SyncReconciler(runOnce 可测;main 双循环,间隔
  SYNC_RECONCILE_INTERVAL_MS env 可调,CN_API_URL 指向 cn 入口):每轮
  先按 album/category/photo/asset 逐类分页拉取 cn reconcile「当前状态
  事件」并直接签名推送到既有 content-batches 管道。sinceVersion 取 0
  (全量当前态)而非 per-type 已应用最大版本——cn reconcile 以类型级
  全局版本阈值过滤,类型内单实体滞后会被 max 掩蔽;ov 的 eventId 幂等
  + 版本单调把已追平实体全部判重(recon 确定性 eventId 重扫零成本),
  偏差登记:计划原文对账返回「缺失清单」,契约已定为可重放 SyncEvent
  数组(免建第二条拉取通道),openapi 描述同步更新
- cn 新增 POST /internal/sync/digest:每相册权威计数 {categoryCount,
  publishedPhotoCount, photoCount, assetCount};ov 比对副本计数(照片
  取 [publishedPhotoCount, photoCount] 区间口径——hidden/deleted 照片
  若从未以 published 态同步过 ov 合法无行,缺席即撤下),不一致相册
  自动触发 cn bootstrap 重建;ov-only 相册(cn 行已删,bootstrap 无法
  表达删除)告警登记不自动处置
- 收敛语义两处落位:① sync-inbox 版本比较改「严格更旧才拒绝」——
  同版本新 eventId(bootstrap/reconcile 快照重发)为当前态幂等重断言
  应用,修复 ov 缺失副本行;A26 不受影响(更旧版本仍拒绝,cn 侧
  entityVersion 与状态一一对应)② album.updated 快照载荷(bootstrap/
  reconcile)携带分类全量清单,ov 存在即整体替换 albums_ov.categories
  ——修复错过的 category 墓碑(Task 3 登记边界:cn 行已删,无现存实体
  事件可重放,唯全量替换可收敛)
- e2e(reconcile.e2e-spec):漏事件追赶(ov v1/cn v5 事件投后即失)→
  清扫追平;digest 修复缺失 photos_ov 行(bootstrap 重发 → 同版本
  重断言重建);digest 修复丢失的分类墓碑(全量清单替换收敛);稳态
  空转(applied=0、无 bootstrap);Task 3 套件保持绿

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 16:18:31 +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