Commit Graph

6 Commits

Author SHA1 Message Date
浅念 46f9d93fd3 fix(app): guard force-column migration against downgrade-then-reupgrade
Review hardening for Task 8 (test-server workflow: testers install older
APKs over a newer DB):

- Old APK opening a v2 database hits drift's downgrade path, which leaves
  the force column in place but writes user_version back to 1. Reinstalling
  the new APK then re-runs onUpgrade(1,2) and addColumn crashes with
  "duplicate column name: force", failing the DB open.
- onUpgrade now checks PRAGMA table_info(upload_tasks) and only adds the
  force column when absent; the normal v1 -> v2 path is unchanged.
- Migration test simulates the downgrade-reupgrade cycle on a file-backed
  database (v2 open -> user_version forced back to 1 -> re-upgrade),
  asserting the open succeeds, legacy rows survive, and the column stays
  writable. RED before the guard (SqliteException: duplicate column name).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 17:27:52 +08:00
浅念 29f3bde9c2 feat(app): wire server dedup - force passthrough + PHOTO_DUPLICATE classification & l10n
Task 8: close the gap between server-side duplicate detection (90dcc58)
and the app.

- ApiClient.mapDioException: normalize 409 wire body reusing
  VALIDATION_FAILED with message=PHOTO_DUPLICATE to
  ErrorCodes.photoDuplicate (message-based; auto-compat if the contract
  later adds a dedicated code)
- l10n zh/en errorPhotoDuplicate; error_l10n maps the code (queue page
  inherits via fallback)
- UploadEngine._classifyFailure: PHOTO_DUPLICATE joins the no-auto-retry
  bucket (retrying the same init body would 409 again); force to skip
  dedup travels only through the enqueue path
- UploadTasks drift table +force column (schema v2, addColumn migration;
  legacy rows default false); UploadTaskDraft/UploadTask/prepare carry it
- enqueueForUpload +forcePaths; duplicate dialog "全部上传" (capture page
  + album detail page) now returns DuplicateDialogChoice so confirmed
  duplicates enqueue with force=true (server adjudicates owner/admin)
- engine init body carries force only when true (absent otherwise)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 17:06:19 +08:00
浅念 1f67b7c818 feat(mobile): UI-01 upload center, capture and auxiliary pages
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-21 17:20:43 +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
浅念 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