Ctrl K
ComunidadGuías…

CHANGELOG.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Changelog

All notable changes to Readest Lite are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [v8.23.4] — 2026-10-22

### Fixed — 超过 10MB 的书籍上传被截断(issue #8)

**问题**:用户部署(Caddy 反代或本机直连)后,超过 10M 的图书不能完整同步到服务器——
服务器落盘的文件只有约 10MB,下载回来无法打开。反复修改反代配置无效。

**根因**(已在本地复现并验证):**Next.js 16 对被 middleware 匹配的、带请求体的请求
做 "cloneable body" 包装**(`next/dist/server/body-streams.js`)。包装缓冲上限由
`experimental.proxyClientMaxBodySize` 控制,**默认 10MB**。请求体超过 10MB 时:

1. 包装流被静默 `push(null)` 提前关闭(仅保留前 10MB,控制台只有一条
   `Request body exceeded 10MB ... Only the first 10MB will be available` 警告)
2. middleware 执行完后,`finalize()` 用**截断后的缓冲流替换原始 `req`**
3. API handler `req.pipe(writeStream)` 只能收到前 10,485,696 字节

`/api/storage/_put`(书籍签名 PUT 直传端点)恰好在 middleware matcher 的
匹配范围内,因此所有 >10MB 的书籍上传(Web / Tauri / Android 壳 App /
浏览器扩展,所有客户端)都被截断。`/api/send/inbox/file`(收件箱文件)与
`/api/readera-import`(ReadEra .bak 导入,上限 200MB)同样受影响。

**修复 1 — middleware matcher 排除大文件二进制端点**(`src/middleware.ts`):

- `/api/storage/_put`、`/api/send/inbox/file`、`/api/readera-import` 从
  matcher 排除,原始请求流直达 API handler,彻底绕过 10MB clone 缓冲
  (同时避免了 clone 双流对大文件的 2 倍内存驻留)
- 安全性不受影响:`_put` 的鉴权由 query 中的 HMAC 签名(key/expires/sig)
  提供;三个端点的 CORS 均由各自 handler 内的 `corsAllMethods` 提供;
  其余 `/api/*`(全部是小 JSON 元数据请求)继续走 middleware

**修复 2 — 恢复 v8.8.0 的 webUpload 自动分块**(`src/utils/transfer.ts`):

v8.8.0 实现的 `webUpload` >5MB 自动分块逻辑在某次重构中丢失(服务端
`_put.ts` 的分块协议 index/total/merge 一直在,但客户端不再分块)。本次恢复:

- 文件 ≤ 5MB:单次 PUT(原路径,零行为变化)
- 文件 > 5MB:切成 5MB 块串行 PUT `&index=N&total=M`,最后发
  `&merge=1&total=M` 触发服务端流式合并
- 进度回调跨块累计,UI 显示连续进度条
- URL 用 `window.location.href` 作 base 解析,兼容相对 URL(本地直连)
  与绝对 URL(`PUBLIC_BASE_URL` 反代场景)

分块的意义:修复 Cloudflare 反代下大文件上传 524 超时的回归(v8.8 原始
动机——慢带宽下每块 ~30 秒内可传完,远低于 CF 100s origin timeout),
并使每块 body(5MB)远小于任何代理层的请求体限制,作为第二道防线。

**测试**(dev server + 真实 API 全链路,md5 校验):

- 整传 3MB / 12MB / 15MB / 30MB:修复前 12MB+ 全部截断为 10,485,696 字节,
  修复后全部完整(大小 + md5 一致)
- 分块 12MB(3 块 + merge):合并完整,parts 目录正确清理
- 收件箱 15MB POST:完整落盘(修复前会被截断)
- 下载 12MB GET `_get`:完整
- 登录 / 常规 API 无回归
- `tsc --noEmit` 0 错误;biome lint/format 通过
- 已有单测失败项(localStorage 环境问题)经 stash 对比确认为预存在,与本修复无关

## [v8.23.0] — 2026-09-12

### Removed

- **有声书内置播放器**:移除 AudiobookPlayer 组件、/api/audiobook/* 端点、bookService 音频短路、BookFormat MP3/M4A/M4B 类型。加到 TODO 待办列表,计划重新开发更稳定的版本
- **RSS 自动 favicon 识别**:移除 favicon.ts 和 feedBook.ts 中的 favicon 抓取逻辑。RSS 书籍封面现在使用默认 RSS 橙色图标

### Fixed

- **超级管理员 ENV 识别**(彻底修复):
  - 根因:`init-admin.mjs` 脚本(容器启动时运行)硬编码 `role = 'admin'`,在 Phase 1 会把已有的 super_admin 降级回 admin
  - 修复:计算 `targetRole = (SUPER_ADMIN_EMAIL == ADMIN_EMAIL) ? 'super_admin' : 'admin'`,不再降级
  - `localAuth.ts` 的 `ensureAdminUser` 也同步修复(v8.22.4 已修但 init-admin.mjs 才是实际运行的脚本)
- **分组管理与书库 GroupBy 融合**:
  - Bookshelf.tsx 在 groupBy=Group 时从 /api/book-groups 拉取 BookGroup DB 条目
  - 将 DB 中的空分组注入书架列表,用户在 GroupManagementModal 创建的分组现在会出现在书库中

### Added

- RSS 防重复:`handleAddFeedSubmit` 增加防重复检查
- README 新增 TODO 待办列表
- README NodeByte 社区置顶(for-the-badge 风格)

## [v8.22.5] — 2026-09-12

### Added — 内置 HTML5 有声书播放器(无需外部服务)

Readest Lite 现在自带内置有声书播放器,不再依赖 Audiobookshelf。直接上传 .mp3/.m4a/.m4b 即可播放。

**新增 API:**
- `GET /api/audiobook/metadata/[bookHash]` — 返回该书的音频章节列表(自动按 fileKey 排序,提取章节标题)
- `GET /api/audiobook/stream/[bookHash]?fileKey=...&token=...` — 流式返回音频文件,支持 HTTP Range(拖动进度条),token 通过 URL 传递(HTML5 `<audio>` 无法设置 Authorization header),24 小时 TTL

**前端组件:**
- 新增 `AudiobookPlayer.tsx`:
  - 进度条拖动跳转
  - 播放/暂停/上一章/下一章
  - 音量调节 + 静音按钮
  - 播放速度(0.75× / 1× / 1.25× / 1.5× / 1.75× / 2×)
  - 章节列表(点击切换)
  - 自动保存进度到 localStorage(每 5 秒),下次打开继续播放
  - 自动连续播放下一章
  - 显示封面、章节标题
- `/player` 页面改为:先调用 metadata API,如果该书有音频文件 → 渲染 AudiobookPlayer;否则显示 3 种播放方式教程

**bookService 改造:**
- 新增音频文件短路:文件扩展名是 .mp3/.m4a/.m4b 时跳过 foliate-js 解析,直接构造最小化 BookDoc
- 新增 `makeAudioBookDoc` 辅助函数(用文件名作为元数据)
- BookFormat 类型扩展:`'MP3' | 'M4A' | 'M4B'`
- `EXTS` 和 `MIMETYPES` 字典也扩展

**导航更新:**
- `useOpenBook.ts`:本地音频书(MP3/M4A/M4B)也走 `/player` 路由
- 新增 `isLocalAudiobook` 和 `isAnyAudiobook` 辅助函数

**README 重构:**
- NodeByte 社区链接放到 README 顶部最显眼位置(for-the-badge 风格)
- 新增「🔊 有声书播放」详细章节,包括 3 种方式 + 播放器功能列表 + 鉴权说明

### 修改
- ITERATION_PROMPT.md 更新到 v8.22.3
- CHANGELOG.md 加 v8.22.3 条目
- README badge 与版本表更新
- package.json: 8.22.2 → 8.22.3

## [v8.22.2] — 2026-09-10

### Fixed — 严重 CORS 错误 + 用户体验改进

**根因修复:reader 页面 CORS 错误(download.readest.com / storage.readest.com)**

- 根因:客户端 fetch 上游 `https://download.readest.com/releases/release-notes.json` 与 `https://storage.readest.com/public/font/dist/...` → 浏览器 CORS 拒绝
- 修复:
  - `helpers/updater.ts` — `checkForAppUpdates` 和 `checkAppReleaseNotes` 在 web 平台(非 Tauri)直接返回 false,不再 fetch 远程
  - `services/constants.ts` — `READEST_CHANGELOG_FILE` / `READEST_UPDATER_FILE` 改为本地相对路径 `/releases/...`,`READEST_PUBLIC_STORAGE_BASE_URL` 改为空字符串
  - `styles/fonts.ts` — `hostedCJKFonts` 改为空数组,避免引用 storage.readest.com 上的 CJK 字体 → 加载会 404 但不会 CORS 报错
  - 新增 `public/releases/latest.json` 与 `public/releases/release-notes.json`(Lite 自有的本地更新日志)

**有声书点击改为应用内教程页**

- 之前点击有声书显示「Lite 不支持有声书」冷冰冰提示
- 改为详细的「3 种播放方式」教程:
  1. 上传本地 MP3/M4A/M4B 文件(含多文件分章节指导)
  2. 配置 Audiobookshelf 服务器流式播放
  3. 导入带朗读的 EPUB
- 加图标 + 详细步骤说明 + 配置示例

**用户中心布局调整**

- 阅读统计卡片(最核心功能)移到第一位(之前在用户管理下方)

**管理员文件管理可折叠**

- AdminFileTransfer 默认折叠,点击标题展开/收起,与回收站一致
- 折叠时显示「点击管理所有用户的文件 — 移动、复制或删除」引导文本

**移动/复制文件到自身的边界情况**

- `/api/storage/move` 与 `/api/storage/copy` 增加检查:targetUserId === file.userId 时跳过,避免「Target file already exists」错误

### 翻译

- 新增 15 个简体中文键(有声书教程 + 折叠 UI)

## [v8.22.0] — 2026-09-10

### Added — 上游 v0.12.7 / v0.12.8 适配移植 + 修复 + 翻译补全

**上游移植(v0.12.7 + v0.12.8 中适合 web Lite 的功能):**

- **ReadEra 注释导入**(来自上游 #6032):
  - 新增 API `POST /api/readera-import`(multipart/form-data,接受 library.json 文件)
  - 在 ImportAnnotationsDialog 新增「ReadEra」入口
  - 流程:上传 ReadEra 备份 zip 内的 library.json → 按书名匹配当前用户库 → 把 citations/bookmarks/position 转为 BookNote 行

- **Notion 笔记同步**(来自上游 #5949):
  - 新增 Prisma 设置字段 `NotionSettings`(accessToken / databaseId / autoSync / lastSyncedAt)
  - 新增 API 路由:
    - `POST/GET/PATCH /api/notion/[...path]` — 通用 Notion API 代理(解决 Notion API 无 CORS 头)
    - `POST /api/notion/sync-all` — 列出所有书籍 + 同步所有 notes 到 Notion 数据库
  - 新增 IntegrationsPanel 入口 + NotionForm 组件:配置 token / databaseId / 自动同步 / 测试连接 / 立即同步

- **带登录的网页小说导入**(来自上游 #6119):
  - 新增 API `POST /api/novel/proxy`(服务器端代理抓取,绕过 CORS)
  - 在 ImportNovelDialog 新增「高级选项」可折叠面板:Cookie 输入框 + 自定义请求头 JSON 输入
  - 当用户填了 cookie/headers 时改走 `/api/novel/proxy` 代理;SSRF 防护:复用 `isBlockedHost`

### 修复(v8.21.0 用户反馈的问题)

- **分组管理点击没反应**:
  - 根因:点击「分组管理」时先关闭 Dropdown,但 Dropdown 关闭会让 ViewMenu 卸载,导致 modal state 丢失
  - 修复:点击时**不关闭** Dropdown,让 GroupManagementModal 用 `createPortal` 渲染到 `document.body`,z-index=200 > Dropdown z-50,全屏覆盖后 dropdown 视觉上看不见
- **有声书点击闪退**:
  - 根因:Next.js 16 要求 `useSearchParams` 必须包在 `<Suspense>` 内,否则 build 时会编译警告 + 运行时崩
  - 修复:`/player` page 加 Suspense wrapper
- **创建用户无所属用户组选项**:
  - 之前 role select 只在 super_admin 才显示,普通 admin 看不到角色选择
  - 修复:role select 永远显示;普通 admin 时 disabled;加提示文本
- **AdminFileTransfer 用户列表可搜索**:
  - 之前的 select 是普通下拉,不能搜索;改用 `<input list="...">` + `<datalist>`,可输入搜索匹配
  - 「选择源用户」与「选择目标用户」都可搜索

### 翻译补全

- 新增 32 个简体中文键 + 48 个 en 键
- 覆盖:有声书页面 / 创建用户角色 / 文件管理 UI / 分组管理 / ReadEra / Notion / 网页小说导入

### 文档

- ITERATION_PROMPT.md 更新到 v8.22.0
- README badge 与版本表更新

## [v8.21.1] — 2026-09-10

### Fixed
- `ViewMenu.tsx` 使用了 `react-icons/md` 中不存在的 `MdFolderManage` 图标,CI 构建失败。改为 `MdCreateNewFolder`。

## [v8.21.0] — 2026-09-10

### Added — 回收站批量 + 管理员跨用户文件管理 + 分组管理

**回收站批量选择:**
- `RecycleBin.tsx` 每行加 checkbox(`selectedIds` state)
- 列表内每项 + Modal 内每项都有 checkbox
- 展开后工具栏显示:选中数量 / 恢复选中 / 删除选中 / 取消选择
- Modal 顶部加:全选 + 批量恢复 + 批量删除(带选中数)
- 批量删除前 `confirm()` 二次确认

**管理员跨用户文件管理:**
- `/api/storage/list` 支持 `?userId=<id>`(看指定用户)与 `?allUsers=1`(看所有用户)
  - 跨用户场景响应附 `user_id` / `user_email` / `user_display_name` 字段
  - 非 admin 调用跨用户参数返回 403
- 新增 `POST /api/storage/move` — 批量移动文件到目标用户(物理 move + File 行 userId/fileKey 变更 + 审计日志)
- 新增 `POST /api/storage/copy` — 批量复制文件到目标用户(物理 copy + 新 File 行 + 审计日志)
- `/api/storage/delete` 支持 `?userId=<id>`(admin 删他人文件)+ `?purge=true`(物理删除跳过回收站)
- 新增 `AdminFileTransfer` 组件,集成到用户中心(仅 admin/super_admin 可见):
  - 顶部用户下拉(默认「所有用户」)+ 文件名搜索框 + 刷新按钮
  - 表格列:勾选框 / 文件名 / 用户(跨用户视图时显示)/ 大小 / 创建日期
  - 表头粘性 + 列表可垂直滚动(max 400px)
  - 工具栏:全选/反选 + 目标用户选择 + 移动/复制/回收/彻底删除
  - 所有批量操作带二次确认

**分组管理(书库三点菜单底部):**
- 新增 Prisma 模型 `BookGroup`(id/userId/name/sortOrder/createdAt/updatedAt)
  - 唯一约束 `userId+name`,索引 `userId+sortOrder`
  - `User` 模型新增 `bookGroups BookGroup[]` 关系
- 新增 API:
  - `GET /api/book-groups` — 列出当前用户的所有分组(按 sortOrder 升序)
  - `POST /api/book-groups` — 创建分组(名称必填,最长 100 字符,防重名)
  - `PUT /api/book-groups/[id]` — 修改分组名 / 排序(同步更新 Book.groupName)
  - `DELETE /api/book-groups/[id]` — 删除分组(同步清空 Book.groupName)
- 新增 `GroupManagementModal` 组件,集成到 `ViewMenu` 底部:
  - 顶部「新分组名称」输入框 + 「添加」按钮(回车提交)
  - 中部搜索框(按名称实时过滤)
  - 每行分组:上下移排序按钮 + 编辑按钮 + 删除按钮
  - 编辑模式直接行内编辑分组名(回车保存 / Esc 取消)
  - 删除前 confirm 提示,提示该分组下的书籍将变为未分组
  - 列表可垂直滚动(max 360px),分组多时不会溢出
  - 底部统计显示当前过滤后分组数

**中文翻译补全:**
- 新增 49 个简体中文键 + 75 个 en 键
- 覆盖:分组管理 / 回收站批量 / 跨用户文件管理 / 权限错误

### Fixed
- 管理员查看用户文件时显示「仅管理员」错误:`/api/storage/list` 已支持 `?userId=` 与 `?allUsers=1`
- 无法跨用户移动/复制文件:新增 `/api/storage/move` 与 `/api/storage/copy` 端点 + UI
- 回收站无法批量恢复/删除:RecycleBin 已加 checkbox + 批量操作工具栏
- 分组管理无搜索/编辑/删除/排序:新建 GroupManagementModal 完整实现

## [v8.19.4] — 2026-09-06

### Added — 阅读统计加密同步 + 有声书 stub 非阻塞 + Admin 用户详情

#### 1. 阅读统计加密同步到所有设备
- 新增 `services/sync/statsSync.ts`,复用 v8.18.4 加密设置同步通道
  (`UserSetting` 表,scope = `'reading_stats'`)
- `store/settingsStore.ts` 在 `saveSettings` 后调度独立的 10 秒
  debounce 推送本地 `page_stat_data` 行
- `hooks/useLibrary.ts` 在 library 加载时拉取并合并到本地
  `StatisticsDb`(last-writer-wins per (bookHash, page, startTime))
- 此前阅读统计仅通过 KOSync 同步最新进度,完整每页历史留在本地

#### 2. 有声书 stub 改为非阻塞
- `services/audiobook/absPairing.ts` 的 `buildAbsPairingSource` /
  `loadAbsPairingSource` 不再抛 `Error`,返回空对象 / 原值
- `services/tts/TTSController.ts` 的 ABS 路径:
  - 动态 `import('@/services/audiobook/absPairing')` 包 try/catch
  - `loadBlob` 返回空 `Blob` 而非 throw
- 此前用户误触 ABS 配对 UI 时 TTS 启动崩溃

#### 3. Admin 用户详情 Modal
- `UserManagement.tsx` 新增 `UserDetailModal`,admin 在 AllUsersModal
  点 chevron 打开
- 展示用户信息(头像 / 邮箱 / 角色 / 创建 / 最后登录 / 配额)+ 书籍列表
  (搜索 + 排序 by title / upload date / file size)+ 回收站条目
- 回收站条目支持多选 + 「Restore Selected」/ 「Permanently Delete
  Selected」
- 新 API:
  - `GET /api/admin/users/[id]/books` — admin-only 列出 target 用户书籍
  - `GET /api/admin/users/[id]/recycle-bin` — admin-only 列出 target
    用户回收站(先自动清理过期的)
  - `POST /api/admin/users/[id]/recycle-bin?action=restore|delete` —
    admin 跨用户恢复 / 永久删除
- 服务端用 `validateUserAndToken` + `isAdmin` 双重校验

#### 4. ITERATION_PROMPT.md 重写
- 重写为约 27000 字符的综合迭代文档(原 9800 字符)
- 覆盖 v8.0 到 v8.19.4 所有设计决策、迁移、API 端点和 Lite 自定义文件清单
- 新增 12 个 zh-CN 翻译条目(Book List / Restore Selected / Upload Date 等)

## [v8.18.9] — 2026-09-06

### Added — 用户头像 + OPDS 布局修复 + 下载任务搜索

#### 1. 用户头像 URL
- `User` 表新增 `avatarUrl` 字段(migration `017_user_avatar.sql`)
- 管理员可在「用户管理」对话框为每个用户设置头像 URL
- 管理员可用环境变量 `ADMIN_AVATAR_URL` 强制覆盖自己的头像(优先级高于 DB 值)
- 头像 URL 接受 `http(s)://` 或 `data:image/*`,**拒绝 SVG**(XSS 风险)
- 用户列表中显示头像,无头像时回退到 `IoPersonOutline` 图标
- 用户中心「我的资料」也优先显示 `user.avatarUrl`

#### 2. OPDS 导航栏窄屏溢出修复
- 主页按钮 / 前进 / 后退按钮被搜索框挤压溢出屏幕
- 搜索输入框改为 `flex-1 + min-w-0`,让其在窄屏优先收缩
- 导航按钮和右侧操作分别包在 `flex-shrink-0` 容器中
- 父级 `<header>` 添加 `overflow-hidden`

#### 3. 下载任务搜索
- `DownloadTasks` 顶部新增搜索框,支持按 URL / 文件名 / 状态过滤
- 用 `useMemo` 缓存过滤结果,避免每秒轮询时重复过滤
- 搜索时显示全部匹配项;不搜索时仍只显示前 3 条

#### 4. 用户名创建校验
- 创建/编辑用户时校验 `displayName` 不能包含 `@`、尖括号、引号、斜杠、控制字符
- 避免和 email 混淆或注入到 HTML 渲染中
- 共享校验工具 `utils/userValidation.ts`

### Verified — Quota / Login error / Subscription removal

#### 5. 管理员配额强制执行 ✓
- `pages/api/storage/upload.ts` 已强制 `storageQuotaMB`(0 = 无限)
- `app/api/translate/google/route.ts` 已强制 `translationQuotaKB`(0 = 无限)
- `utils/downloadRunner.ts` 下载完成后写 `File` 表记录 `fileSize`,计入用户存储

#### 6. 登录错误统一 ✓
- `utils/localAuth.ts` 的 `signInWithPassword` 对「用户不存在」和「密码错误」
  统一返回 `Invalid login credentials`,避免用户枚举攻击

#### 7. 移除订阅管理 ✓
- `components/settings/ControlPanel.tsx` 无订阅/Feed 条目(已确认)
- `app/library/components/SettingsMenu.tsx` 无订阅/Feed 条目(已确认)
- Lite 所有用户视为 Pro,不需要订阅管理

## [v8.18.4] — 2026-09-05

### Added — 分享永久/自定义日历 + 加密设置同步 + 自动撤销 + ITERATION_PROMPT 更新

#### 1. 分享过期时间 — 永久 + 自定义日历
**用户原话**:「分享书籍不是还支持无限过期时间和自定义过期时间吗,还有个日历选择框」

**实现**:`ShareBookDialog.tsx` 的「Expires in」现在有 5 个选项:
- `1 天 / 3 天 / 7 天` — 预设(原有)
- `Permanent`(永久)— `expirationDays=0`,服务端把 `expiresAt` 设为 `9999-12-31`
- `Custom`(自定义)— `expirationDays=-1`,弹出 `<input type='date'>` 日历选择器,
  客户端计算剩余天数(1-365)传给服务端

服务端 API 已支持 `expirationDays` 0 (永久) 和 1-365 任意整数。

#### 2. 接收方副本完全独立 + owner 删除自动失效
**用户原话**:「原用户的书与接受分享用户数据是毫无关联的」
「如果有个用户保存了,原来用户删除了那还在不在」

**实现**:
- 接收方 import 时,服务端 `copyObject()` 把文件字节级复制到接收方命名空间
  (`<recipientUid>/Readest/...`)+ 插入独立 DB row
- **接收方的副本完全独立** — 原 owner 删除自己的书**不影响**接收方的副本
- 原 owner 删除书籍时,`pages/api/storage/delete.ts` 自动 revoke 该
  `(userId, bookHash)` 的所有活跃 `BookShare` 行(`revokedAt = now()`)
- 接收方访问已 revoke 的分享链接 → 返回 410 `revoked`
- **接收方已保存的副本不受影响** — 独立 owner 的独立 row,原 owner 删除不级联

#### 3. feed:// source_deleted bug 修复
`shareServer.ts` 的 `resolveActiveShare` 现在先检查 `row.revokedAt`
(由 `delete.ts` 自动 revoke 设置),所以 feed:// 分享的 owner 删除书后,
接收方访问正确返回 410 `revoked` 而非跳过检查继续可用。

#### 4. 加密设置同步(UserSetting 表)
**用户原话**:「每个用户的用户前端设置和配置全部加密存储服务端实现同步安全一点」

**实现**:
- 新增 `UserSetting` Prisma model(migration `016_user_settings.sql`)
- 按 `scope` 分区存储:
  - `system` — SystemSettings(KOSync、Readwise、OPDS、proxy 等)
  - `global_view` — globalViewSettings(字体大小、主题、布局)
  - `global_read` — globalReadSettings(翻页、自动滚动、TTS 等)
- `encryptedPayload` = base64(JSON(CipherEnvelope)),用密码派生的 AES 密钥加密
- **服务端只存密文,不解密** — 跨设备同步时其他设备 GET 后在客户端解密
- API:
  - `GET /api/settings?scope=system` — 取最新密文
  - `PUT /api/settings?scope=system` — 上传密文(upsert)

#### 5. ITERATION_PROMPT.md 完整更新
**用户原话**:「最后记得更新完整提示词」

更新内容:
- 版本号 bump 到 v8.18.4
- Lite 自定义文件清单新增 v8.18.4 的文件(favicon.ts、feedBook.ts、
  RemoteDownloadDialog.tsx、ShareBookDialog.tsx、delete.ts、settings/{index,save}.ts)
- 12 个关键设计决策(新增 #6 接收方独立+自动 revoke、#7 永久+日历、#8 加密设置同步)
- 后端安全清单新增 #9 分享自动 revoke
- CI 失败排查新增 #7 TS6133 noUnusedLocals
- 新增「数据库 Migration 清单」段落

#### 6. 5 条新中文翻译
- `Permanent` → 「永久」
- `Custom` → 「自定义」
- `Pick date` → 「选择日期」
- `Please pick a date` → 「请选择一个日期」
- `Date must be at least 1 day from now` → 「日期必须至少从现在起 1 天后」
- `Date must be within 365 days` → 「日期必须在 365 天内」

### CI Status
- `CI` workflow: ✅ success
- `Docker Image` workflow: ✅ success
- `CodeQL Advanced`: ❌ failure (pre-existing, not blocking)

## [v8.18.3] — 2026-09-05

### Added — 第三轮用户反馈功能 + 修复

#### 1. RSS feed 书籍分享 — 不需要文件
**用户原话**:「在分享的时候,它会显示找不到这本书的原文件这个你要修一下」
「另外一个用户在看到这本书的时候转存的时候是直接在数据库中再复制一份数据,同样的数据到另外一个用户数据表」
「相同的资源,它不会重复重复再复制一次,而是共用一份资源」

**实现**:
- `BookShare` schema 新增 `bookUrl` 列(migration `015_book_share_url.sql`)
- `share/create/route.ts` 在 `bookUrl` 是 `feed://` 时跳过 file lookup,直接创建「无文件分享」
- `shareServer.resolveActiveShare` 检测到 feed:// 时返回 `isFeedBook=true` + descriptor URL,不需要 `bookFile`
- `share/[token]/download/route.ts` 对 feed:// 分享返回 descriptor JSON(不是文件 redirect)
- 接收方根据 descriptor + bookHash 重建订阅,不需要下载文件
- ACL 安全:owner 删除书籍并选「云端 + 数据删除」时,原 file lookup 返回 `source_deleted` → 接收方访问返回 410

#### 2. RSS feed 书籍上传按钮 + 自动同步
**用户原话**:「它并没有上传按钮,你要加进去,目前似乎并不能真的上传上去」
「默认它是选自动上传或下载的他就一起上传下载,反正跟其他的没区别,一样的规则」

**实现**:
- `getBookContextMenuItemIds` 现在对 feed:// 书籍也暴露 `upload` / `download` / `share` 菜单项
- `cloudService.uploadBook()` 已有 feed:// short-circuit(v8.18.1):上传封面 + 标记为已上传,跳过 blob 上传
- `cloudService.downloadBook()` 同理:feed:// 书籍只下载封面,标记 downloadedAt
- 自动同步行为与普通书籍完全一致:sync engine 跑 `library.json` 时正常处理

#### 3. RSS 站点 favicon 自动识别
**用户原话**:「目前并没有看到rss类数据能够自动识别站点的图标,要增加所有常用站点图标识别语法」
「没有显示在这本书的对应书的封面的一部分位置当中」

**实现**:
- 新增 `services/rss/favicon.ts` 的 `fetchFeedFavicon()` — 按以下顺序查找 favicon:
  1. `<link rel="icon" type="image/svg+xml">`(SVG 优先)
  2. `<link rel="icon">`(任意类型)
  3. `<link rel="apple-touch-icon">`
  4. `<link rel="shortcut icon">`
  5. `/favicon.ico`
- 走 `isProxyEnabled()` 时通过 `/api/proxy/resource` 服务端代理(绕过 GFW)
- 5 秒超时,失败时回退到默认 RSS 橙色图标
- `feedBook.ts` 的 `ensureFeedBookCover()` 在生成 SVG 封面时把 favicon 作为 avatar 嵌入

#### 4. 移除批量下载的「Advanced Options」
**用户原话**:「批量下载不需要另外高级选项,因为所有的高级选项语法都已经包括在每一个url的输入框中了」

**实现**:
- `RemoteDownloadDialog.tsx` 的「批量下载」tab 移除全局 Cookies/Headers 输入区
- 每个 URL 行自带 `URL | cookie:VALUE | header:Key: VALUE` 指令
- 帮助文案改为「每个 URL 行自带 cookie / header 指令 — 不需要全局高级选项」

#### 5. 64 条新中文翻译
覆盖所有 v8.18.x 新增的「Readest Lite」品牌字符串 + 阅读统计 / 下载任务 / 管理员 UI / feed 术语。

#### 6. ITERATION_PROMPT.md — 完整 Lite 维护者提示词
新增仓库根目录的 `ITERATION_PROMPT.md`,包含:
- Lite 与上游的核心差异(绝不改回上游)
- Lite 自定义文件清单(绝对不能被上游覆盖)
- 10 个关键设计决策(相对 URL、getRemoteBookFilename 非空、代理强制登录、feed:// 分享/同步、批量 URL 语法、favicon 检测、笔记面板优雅降级、设置同步等)
- 后端安全清单(认证、CORS、输入校验、用户隔离、SSRF、配额、签名 URL TTL)
- CI 失败排查清单(按错误类型分类的解决方案)
- 版本管理 + 文档清单

#### 7. 修复「Readest Lite Lite」双重品牌 bug
`o/page.tsx` 中 sed 把 'Readest app' 误改成 'Readest Lite Lite app',已修复为 'Open in Readest Lite app'。

### CI Status
- `CI` workflow: ✅ success
- `Docker Image` workflow: ✅ success
- `CodeQL Advanced`: ❌ failure (pre-existing, not blocking)

## [v8.18.2] — 2026-09-05

### Fixed — 第二轮用户反馈 bug 修复

#### 1. 中文文件名上传报「Invalid fileName」(严重 — 用户原话:「人教版】八年级上册(2025秋版)语文电子课本-页面-1 Invalid fileName」)
**根因**:`getRemoteBookFilename()` 在 `getStorageType() === 'local'` 时返回空字符串 `''`,导致 cloud path `cfp = 'Readest/Books/'` 出现空段,被 `isSafeObjectKeyName` 拒绝('Invalid fileName')。

**修复**:`local` 分支也按 `r2` 规则生成 `hash/safe-name.ext`,cloud path 永远有真实文件名段。

#### 2. 下载报「File not found」(严重 — 用户原话:「Fundamental-Accessibility-Tests-Basic-Functionality-v2.0.0 File not found」)
**根因**:上传时写进 DB 的 `fileKey` 是 `<uid>/Readest/Books/<hash>/`(无文件名段),下载请求的 `fileKey` 是 `<uid>/Readest/Books/<hash>/<title>.ext`,两者不匹配 → 404。

**修复**:修复 #1 后上传写入的 `fileKey` 与下载请求一致,匹配命中。

#### 3. 站点名称 / 安装应用名称仍显示「Readest」
**修复**:完整梳理所有用户可见字符串 + 元数据:
- `public/manifest.json`:name / short_name / description → Readest Lite
- `app/layout.tsx`:title / description / template / appleWebApp / openGraph / twitter → Readest Lite + cshdotcom.github.io URL
- `o/page.tsx` / `s/page.tsx`:'Open in Readest' / 'Readest logo' / 'Opening Readest' → Readest Lite
- `SettingsMenu` / `BookMenu`:'About Readest' / 'Download Readest' / 'Upgrade to Readest Premium' / PIN 提示 → Readest Lite
- `Annotator` / `ExportMarkdownDialog` / `ImportAnnotationsDialog`:'Readest annotations file' / 'Exported from Readest' / 'Select Readest Annotations' → Readest Lite
- `BooknoteItem` / `ShareLanding` / `commandRegistry`:所有 'Open in Readest' / 'Readest logo' → Readest Lite

#### 4. 删除服务条款 + 隐私政策(用户原话:「把服务条款 隐私政策删除」)
**修复**:移除 `LegalLinks` 组件 + 在 `user/page.tsx` 中的使用。Lite 是自托管,无外部 ToS。

#### 5. 删除文章的批注点击崩溃 → 优雅降级(用户原话:「批注和划线的总的那个看板里面还是有的,只是点击之后无法打开到对应的内容,这里可能会引发一个错误,但是要让系统忽略这里的错误」)
**修复**:`BooknoteItem.handleClickItem` 用 try/catch 包住 `goTo(cfi)`。若 RSS 文章已被删除但批注仍在 `config.json`,导航失败时显示 toast「The highlighted location is no longer available in this book.」,不再让面板崩溃。

#### 6. 阅读统计清除功能(用户原话:「增加一个阅读统计数据清除功能删除按钮放在那个危险操作里」)
**修复**:
- 新增 API 端点 `DELETE /api/usage/stats` — 删除当前用户的所有 `StatPage` + `UsageStat` 行(需 Bearer auth)
- `AccountActions.tsx` 在「Danger Zone」分区最前面加「Clear Reading Statistics」按钮
- 确认弹窗说明会删除哪些数据(保留书籍、进度、批注)
- 成功后自动刷新页面让 UI 卡片更新

#### 7. 翻译 / 词典代理必须登录验证(用户原话:「翻译代理,词典代理这些是必须要登录后的用户才能使用的」)
**已验证**:`/api/proxy/wiki` / `/api/proxy/resource` / `/api/translate/google` 三个端点全部调用 `validateUserAndToken(authHeader)`,未登录返回 401。无任何公开访问路径。

#### 8. 设置同步说明(用户原话:「同步功能本身就有点问题,就是用户在设置里的修改似乎并没有被同步」)
**说明**:`settings` replica 在 `DEFAULT_SYSTEM_SETTINGS.syncCategories.settings = true` 默认开启。跨设备设置同步需要用户先在「集成 → 阅读同步」中配置 WebDAV / S3 等云同步 provider。没有 provider 时,设置只保存在本地(Lite 不带 Readest Cloud)。

### CI Status
- `CI` workflow: ✅ success
- `Docker Image` workflow: ✅ success
- `CodeQL Advanced`: ❌ failure (pre-existing, not blocking)

## [v8.18.1] — 2026-09-05

### Fixed — 用户反馈的关键 bug 修复

#### 1. 客户端下载报「文件找不到」(严重)
**根因**:`getStorageBase()` 在 `PUBLIC_BASE_URL` 未设置时返回 `http://127.0.0.1:8225`(容器内地址),浏览器无法访问该地址,导致下载预签名 URL 全部 404。

**修复**:默认返回相对 URL(`/api/storage/_get?...`),浏览器自动按当前 origin 解析。需要绝对 URL 的场景(如 `NextResponse.redirect`)由调用方传入 `requestOrigin`,从请求 URL 派生 origin。

**影响文件**:
- `utils/localStorage.ts` — `getUploadSignedUrl` / `getDownloadSignedUrl` 新增可选 `requestOrigin` 参数
- `app/api/share/[token]/{download,cover,og.png}/route.ts` — 传入 request origin

#### 2. 长按书籍无法唤出操作栏(严重)
**根因**:`BookshelfItem` 的 `onLongPress` 在 web 平台只调用 `handleSelectItem()`(进入选择模式),因为 `hasContextMenu` 在 web 上为 false。用户长按看不到任何操作菜单。

**修复**:web 平台长按现在会弹出 `BookContextMenuPopup`(与 Linux 桌面一致),用户可看到 Delete / Upload / Download / Details / Share 等操作。`useLongPress` hook 现在会把指针起始位置传给 `onLongPress` 回调。

#### 3. RSS / feed 书籍无法上传(严重)
**根因**:`feed://` 书籍是有意设计的无文件书籍(只存元数据)。`uploadBook()` 试图上传(不存在的)文件 blob,抛出 "Book file not uploaded" 错误。

**修复**:检测到 `book.url.startsWith('feed://')` 时,跳过 blob 上传,只上传封面(如有),并标记书籍为已上传。`downloadBook()` 同样跳过 blob 下载。

#### 4. 多处显示「Readest」而非「Readest Lite」
**修复**:以下用户可见字符串全部改为「Readest Lite」:
- AppLockScreen:「Unlock Readest」→ 「Unlock Readest Lite」
- AppLockScreen:「Readest is locked...」→ 「Readest Lite is locked...」
- AppLockDialog:「every time you open Readest」→ 「every time you open Readest Lite」
- ControlPanel:「Help improve Readest」→ 「Help improve Readest Lite」
- IntegrationsPanel:「your Readest account」→ 「your Readest Lite account」
- IntegrationsPanel:「Connect Readest to external services」→ 「Connect Readest Lite to external services」
- UpdaterWindow:「A new version of Readest is available」→ 「A new version of Readest Lite is available」
- UpdaterWindow:「Readest {{newVersion}} is available」→ 「Readest Lite {{newVersion}} is available」
- UpdaterWindow:「What's New in Readest」→ 「What's New in Readest Lite」

#### 5. 翻译词典代理(Server Proxy)丢失
**根因**:v0.12.1→v0.12.6 端口覆盖了 google/wikipedia/wiktionary/chineseDict provider,使其直接 fetch 目标 URL,绕过了 Lite 的服务器代理。

**修复**:
- 恢复 `services/translators/providers/google.ts` — proxyEnabled=true 时走 `/api/translate/google`
- 恢复 `services/dictionaries/providers/{wikipedia,wiktionary}Provider.ts` — 走 `/api/proxy/wiki`
- 恢复 `services/dictionaries/chineseDict.ts` — 走 `fetchViaWikiProxy`
- IntegrationsPanel 新增「Network」分区,含「Server Proxy」开关

#### 6. About 页面三个网站排版优化
**修复**:从竖直堆叠改为 3 列网格布局(`sm:grid-cols-3`):
- 桌面端:三个网站卡片并排
- 移动端:自动堆叠
- 移除了冗余的 URL 文字(改为更简洁的「GitHub 仓库 / 原版仓库 / 官网」)

#### 7. v0.12.6 新增功能中文字符串
**修复**:在 `public/locales/zh-CN/translation.json` 中新增约 94 条中文翻译,覆盖:
- 阅读统计(Reading Statistics / Sync History / Book Ranking 等)
- 下载任务(Download Tasks / Pause All / Resume All / Retry All Failed 等)
- 管理员 UI(Create User / Edit User / Storage Quota 等)
- 网络代理(Network / Server Proxy)
- 品牌相关(About Readest Lite / Sign in to Readest Lite 等)

### CI Status
- `CI` workflow: ✅ success
- `Docker Image` workflow: ✅ success
- `CodeQL Advanced`: ❌ failure (pre-existing, not blocking)

## [v8.18.0] — 2026-09-05

### Added — 上游 v0.12.1 → v0.12.6 完整合并(约 95%)

**主要新功能(来自上游 v0.12.6 的 42 个 feat PR):**
- Notebook as a linked writing workspace(#5928)
- Customizable keyboard and mouse bindings(#5907)
- Lyric-style sentence view in the Read Aloud player(#5755, #5908)
- Select chapters when importing web novels(#5892)
- Jump from a footnote popup to the location in the book(#5889)
- From Web Browser import with an in-app browser(#5775, #5870)
- TOC wrap long headings onto multiple lines(#5858)
- Pair an Audiobookshelf audiobook and read along(#5807, #5856)— *stub only in Lite*
- Select text across PDF pages(#5809, #5831)
- Show the book cover full screen from the sidebar(#5813, #5827)
- Join PDF line wraps into paragraphs when copying(#5814, #5828)
- RSVP exact WPM entry and 10 WPM nudge(#5820, #5825)
- PDF page labels as reference pages(#5822, #5824)
- Expose book title/series as data attributes for custom UI CSS(#5776, #5806)
- Inline note editing(#5780)
- data-eink attribute for per-device custom CSS(#5803)
- Audiobookshelf integration(#5801)— *stub only in Lite*
- Bundled dictionary plugin and Yomitan support(#5764)— *stub only in Lite*
- OPDS auto-download toggles and catalog reordering(#5746, #5760)
- Pair local audiobooks with ebooks(#5754)— *stub only in Lite*
- Hide-covers privacy option for the bookshelf(#5733)
- Right-to-left page order for fixed-layout books(#5712)
- Resume Auto Scroll when reopening a book(#5631, #5710)
- Optional document metadata on KOSync progress uploads(#5704)
- Symmetric minimal mini-player with centered play button(#5707)
- Cloud shelves and safe provider deletion(#5701)
- LocalSend receive and send for KOReader devices(#5611, #5687)— *stub only in Lite*
- Smooth Auto Scroll at low speeds(#5679)
- Home/End jump to start/end of book(#5660, #5673)
- Flash the target of in-page footnote jumps(#5647, #5655)
- LocalSend send/receive over LAN(#5611)— *stub only in Lite*
- Download progress overlay on book covers(#5736)
- Queue chapter downloads with per-book persistence(#5690)
- En-hu / En-vi Wordlens gloss packs(#5737, #5738)— *skipped, no wordlens in Lite*
- Nix packaging(#5605)— *skipped*
- Reference pages support(#5822)
- 「Nearby BookDrop」branding(#5915)
- Tier stat_pages history into R2 segments(#5835)— *stub only in Lite*
- Add From Web Browser import(#5775)— *stub only in Lite*
- Audiobookshelf audiobook streaming and podcasts(#5801)— *stub only in Lite*
- Brand the LAN transfer as Nearby BookDrop(#5915)

**基础架构大升级:**
- Tailwind CSS 3 → 4(CSS-based config, removed tailwind.config.ts)
- daisyUI 4 → 5(major upgrade)
- @tailwindcss/postcss 替换 tailwindcss + autoprefixer
- TypeScript 7(@typescript/native-preview / tsgo)
- React 19.2.5 → 19.2.8
- Next.js 16.2.6 → 16.3.3
- @tauri-apps/api 2.10.1 → 2.11.1

**新增依赖:**
- @noble/hashes(crypto)
- katex + marked-katex-extension(math rendering)
- @tailwindcss/postcss(Tailwind v4 PostCSS plugin)

**移除依赖:**
- autoprefixer(Tailwind v4 内置)
- @sentry/cli(无 Sentry 集成)
- @googleapis/androidpublisher(无 Play Store 发布)
- @tauri-apps/plugin-biometric(无生物识别)
- music-metadata(无 audiobook pairing)

**为 Lite 创建 stub 的不适用功能:**
- Audiobookshelf (ABS) — server integration, native-only(AudiobookController/storage/mapping/AudiobookClock/absPairing + store/absServerStore)
- LocalSend — LAN transfer, native-only(devicePrefs + LocalSendManager + LocalSendForm + store/localsendStore)
- Yomitan dictionaries — native plugin system(services/dictionaries/plugins/* + services/plugins/*)
- Wordlens gloss packs — Hungarian, Vietnamese
- Nix packaging
- KOReader plugin (Lua + Rust binary)
- Stats archive (R2 + PostgREST)(libs/statsArchive + app/api/stats/*)
- Audiobook pairing (pairedAudiobook service)
- Web browser in-app import (Tauri-only)(WebSourcesDialog + useWebBrowserDownloads)
- Google Drive / OneDrive cloud sync providers(stubbed connect flows returning not-supported)
- BookOrbit (KOReader cross-device sync) — full type + service kept; UI gated by stub
- iCloud — already stubbed in v0.12.1
- S3 — already stubbed in v0.12.1

**类型系统扩展(v0.12.6):**
- `Book`:absMediaType ('podcast'), episodeCount
- `BookConfig`:audiobook (PairedAudiobook), hardcover (HardcoverBookLink)
- `BookFormat`:add 'ABS'
- `BookNoteType`:add 'notebook'
- `KeyBinding`:ctrlKey/altKey/shiftKey/metaKey/altGraphKey (customizable bindings)
- `SyncCategory`:add 'abs_server'
- `BookOrbitSettings`:export as named interface + add strategy/customHeaders
- `SystemSettings`:add absServers, webSources
- `AppService`:add stats(path, base), unavailableRootDir, isRootDirUsable,
  supportsCoverThumbnailOptimization, requestCoverThumbnail
- `libraryStore.LibraryState`:add coverThumbnails + setBookCoverThumbnail
- `ImportFromFolderResult`:add autoImport (optional)
- `ConnectGoogleDriveResult/ConnectOneDriveResult`:add accountLabel
- `parseBookDeepLink`:optional autoplay flag
- `ReplicaAdapter` (absServer):implement full interface
- `HtmlAudioClock`:implement NarrationTrackPlayer (play/pause/addEventListener)
- `CryptoSession`:copy upstream version with invalidatePassphrase
- `isWrongPassphraseError`:add to libs/errors.ts
- `sha256File`:signature changed to (file: Blob, chunkSize)

**修复 / 改进:**
- tsconfig:添加 rootDir '../..'(用于 js-mdict 包导入)
- DEFAULT_SYSTEM_SETTINGS:添加 absServers: [], webSources: []
- BaseAppService:添加 stats 方法实现
- appLocalStore:恢复 markBooksDownloaded 实现(v8.17 已有,bulk copy 时丢失)
- bookorbit 默认设置:添加 strategy: 'prompt'
- AccountActions:onConfirmDeleteAllBooks 设为 optional
- Bookshelf:handleBookPurge + contentSearch + onSearchContents 设为 optional
- 删除 user/components/PlanCard.tsx 和 PlansComparison.tsx(上游付费计划组件)
- 删除 app/api/stripe/(Lite 无 Stripe 付费计划)
- 删除 100+ 个测试文件(针对 ABS/audiobook/localsend/yomitan/plugins 等不适用功能)

### CI Status
- `CI` workflow: ✅ success
- `Docker Image` workflow: ✅ success
- `CodeQL Advanced`: ❌ failure (pre-existing, not blocking)

## [v8.17.0] — 2026-09-04

### Added — 上游 v0.11.20 → v0.12.1 完整合并

**新功能(来自上游 v0.12.1):**
- YAML frontmatter 元数据提取(#5344)
- TOC 标题深度可配置(#5363)
- Markdown 脚注支持(marked-footnote 插件)
- Column Gap 重命名为更直观的语义(#5315)
- Google 翻译保留原文格式(#5556)
- footer 切换进度条(#5466)
- 图片描述显示(#5472)
- 搜索结果折叠分组(#5282)
- 段落模式字体设置(#5403)
- TTS mini player(#5446)

**基础架构:**
- 50+ 个新文件复制自上游 v0.12.1(reader 组件、settings 模块、tts providers、cloud sync adapters)
- 236+ 个修改文件按上游 v0.12.1 重新对齐
- 引入 marked-footnote、marked-katex-extension、katex、react-colorful 依赖
- Webpack alias:marked-katex-extension、react-colorful、tauri-plugin-turso(stub)

**为 Lite 创建 stub 的不适用功能:**
- BookOrbit(KOReader 设备间同步服务,无 web 端)
- iCloud / GoogleDrive / OneDrive / S3 客户端集成(Lite 用本地文件系统替代)
- clipSignIn(Apple Sign-In,需要 native)
- cloudSync 上游 sync 协议(需要 Supabase Realtime)

**类型系统扩展:**
- `Book` 类型添加 `hasNarration`/`duration`/`metadataUpdatedAt`/`groupUpdatedAt`/`fileSyncDeletionRequestedAt`/`altPaths`
- `SystemSettings` 添加 `librarySkeuomorphicCovers`/`libraryHideCovers`/`autohideCursor`/`googleDrive`/`s3`/`onedrive`/`icloud`/`bookorbit`/`readestCloud`
- `BookLayout` 添加 `scrolledDirection`/`autoScrollRunning`
- `ViewSettings` 添加 `ttsSentenceGap`/`ttsParagraphGap`/`ttsUseNarration`
- `NoteExportConfig` 添加 `exportFormat`/`includeCoverImage` + `NoteExportFormat` 类型
- `KOSyncSettings` 添加 `customHeaders`/`sendMetadata`
- `ReadwiseSettings` 添加 `includeCoverImage`
- `ReaderStore` 添加 `bottomBarTab`/`setBottomBarTab`
- `AppService` 添加 `databaseExists`/`deleteDatabase`/`hasAmbientLightSensor`
- `TransferItem` 添加 `cancelReason`
- `FileSyncState` 添加 `lastErrorByKind`/`switchSync`/`setLastError`

**修复 / 改进:**
- Dockerfile 添加 `NODE_OPTIONS=--max-old-space-size=4096` 避免 OOM
- tsconfig 添加 `types/*.d.ts` include + tauri-plugin-turso stub path alias
- foliate-js `textWalker` 类型改为 `any`(上游 loose inference 兼容)
- `runMigrations` 添加可选 `settings` 参数

### Removed — Lite 不适用的功能
- BookOrbit / iCloud / GoogleDrive / OneDrive / S3 cloud sync UI(用 stub 替代避免编译错误,UI 入口隐藏)
- clipSignIn(Apple Sign-In native 实现)

### CI Status
- `CI` workflow: ✅ success
- `Docker Image` workflow: ✅ success
- `CodeQL Advanced`: ❌ failure (pre-existing, not blocking)

## [v8.13.1] — 2026-07-09

### Added — 翻页动画补全 + 关于页面版本号修复 + bookService 崩溃修复

#### PR #4940 — 翻页动画(幻灯片/卷页)— JS-only port
- **新增文件**:`utils/pageCurl.ts`(CSS mesh curl 回退)、`utils/pageSlide.ts`(CSS slide overlay)、`app/reader/hooks/useCapturedTurn.ts`(触摸拦截器)、`app/reader/utils/capturedTurn.ts`(captured-page 管线)
- `types/book.ts`:新增 `PageTurnStyle` 类型(`'push'|'slide'|'curl'`)+ `pageTurnStyle` 字段
- `services/constants.ts`:`DEFAULT_VIEW_CONFIG` 加 `pageTurnStyle: 'push'`
- `utils/bridge.ts`:加 `captureWebviewRegion`(web 端 reject,回退 CSS curl)
- `FoliateViewer.tsx`:调用 `useCapturedTurn` + `applyPageTurnAttributes`,移除 inline no-swipe 块
- `BooksGrid.tsx`:加 `data-view-transition-root` 属性
- `ControlPanel.tsx`:加 Animation Style SettingsSelect(Push/Slide/Page Curl,按 `supportsViewTransitionGroup` gating)
- **跳过原生代码**(swift-rs/Rust/Kotlin/Swift)— Lite 是 web-only

#### PR #5000 — gate captured slide/curl on scrollLocked
已包含在复制的 `useCapturedTurn.ts` 中:instant highlight 锁定滚动时,slide/curl 翻页也 honor 锁定。

#### 关于页面版本号修复(用户反馈)
- **问题**:「关于」页面显示 `Version 0.11.4`(上游版本号),不是 Lite 真实版本
- **根因**:`getAppVersion()` 读 `apps/readest-app/package.json` 的 `version` 字段,自 fork 以来一直卡在上游的 `0.11.4`
- **修复**:`package.json` version 从 `0.11.4` 改为 `8.13.1`。未来每次发版必须同步更新此字段

#### PR #4962 — bookService 并发导入崩溃修复(READEST-H)
- **问题**:两个并发导入同一本书时,`createDir` 竞争导致 Windows 上报 "Cannot create a file when that file already exists"
- **修复**:`bookService.ts` 的 `createDir` 改为幂等递归 `createDir(path, base, true)`,替代 check-then-create

### CI Status
- ✅ Docker Image workflow — build-and-push success
- ✅ CI workflow — Smoke test success
- 镜像已推送:`ghcr.io/cshdotcom/readest-lite:8.13.1` / `8.13` / `latest`

---

## [v8.13.0] — 2026-07-08

### Added — 上游 Readest v0.11.18 非覆盖式合入

#### 合入方法
按用户要求采用**非覆盖式**合入:逐条研究上游 v0.11.18 的 50 个 commits,按 PR 号定位上游文件改动,分类为「安全合入 / 需适配 / 与 Lite 冲突 / 不适用」,然后一个一个文件改过来,绝不粗暴复制上游文件覆盖 Lite 自定义代码。

#### 第一批:阅读器核心修复(7 PRs)
- **PR #4910** — 打开注解深链时切换到目标书(`useOpenAnnotationLink` scope 到 bookKeys + `useBooksManager.openBookInReader` 接受可选 cfi + `goToCfiWhenReady` 等待视图 init)
- **PR #4912** — 区分双指滚动与捏合缩放(`useIframeEvents` 延迟决策:24px 死区 + 12px 平移阈值,pinch 确认后重新基线距离)
- **PR #4911** — 暗色模式下浅色 PDF 的页眉/页脚可读性(`SectionInfo`/`ProgressBar` 用 `text-white/75 mix-blend-difference`)
- **PR #5001** — 页边距收缩到安全区内(fixed-layout 用 `text-base-content`,reflowable 保持 blend;`HintInfo` 移除早返回 + `line-clamp-1`;`LayoutPanel` 减去 `gridInsets`;`FoliateViewer.applyMarginAndGap` fixed-layout top=0)
- **PR #4989** — View Transitions API 按运行时能力开关(新增 `utils/viewTransition.ts` + `supportsViewTransitionsAPI`/`supportsViewTransitionGroup` 字段 + `useAppRouter` 改用能力检查而非 `isLinuxApp`)
- **PR #4955** — 中键自动滚动(新增 `useMiddleClickAutoscroll` hook + `AutoscrollIndicator` 组件 + `autoscroller.ts` 的 `Autoscroller` 类 + `iframeEventHandlers` 加 `handleMousemove`/`handleAuxclick` + `types/view` 加 `scrollProp`)
- **PR #4998/#4999** — Auto Scroll 阅读模式(新增 `useAutoScroll` hook + `AutoScrollControl` 浮动控制条 + `PacedScroller` 类 + `constants` 加 `AUTO_SCROLL_*` + `types/book` 加 `autoScrollSpeed` + `readerStore` 加 `autoScrollEnabled` + `shortcuts` 加 Shift+A + `ViewMenu` 加 Auto Scroll MenuItem)

#### 第二批:TTS 大改版(5 PRs,作为一组合入)
- **PR #4931** — Edge TTS 无缝 Web Audio 播放引擎(新增 `WebAudioPlayer`/`pcm`/`timeStretch`/`ttsDuration`/`SectionTimeline`;`edgeTTS` 重构 `createAudioData`;`EdgeTTSClient` 改用 WebAudioPlayer;`TTSController` 加 timeline/seek/getPlaybackInfo API)
- **PR #4941** — 关书后 TTS 继续播放(新增 `TTSSessionManager` + `ttsMediaBridge` + `NowPlayingBar`;`TTSController` 加 detachView/attachView;删除 `useTTSMediaSession`;`library/page.tsx` 4 处手术式插入)
- **PR #4957** — 词典弹窗加「朗读」按钮(新增 `wordPronouncer` + `DictionaryPopup`/`DictionarySheet`/`DictionaryResultsView` 加 onSpeak/speaking props)
- **PR #4994** — Android 后台 TTS 媒体控制(TS 侧 only:`mediaSession` 移除 `keepAppInForeground`;`constants`/`types/settings`/`SettingsMenu` 移除 `alwaysInForeground`;跳过原生代码)
- **PR #4996** — TTS 控件重设计为 mini player(新增 `TTSMiniPlayer`/`TTSPlayerSheet`/`TTSScrubber`/`SpeedChips`/`useCountdownLabel`/`usePlaybackInfo`;删除 `TTSBar`/`TTSIcon`/`TTSPanel`;`FoliateViewer` ttsBarHeight→miniPlayerClearance;`types/book`/`constants` 移除 `showTTSBar`)

#### 第三批:图书馆/UI/OPDS/元数据(7 PRs)
- **PR #4947** — 清除已完成/失败/全部时持久化传输队列(`transferManager` 加 `clearCompleted`/`clearFailed`/`clearAll` 方法 + `useTransferQueue` 改用 transferManager)
- **PR #4831/#4913** — 主题模式切换器重设计为分段控件(`ThemeModeSelector` 重写为 radiogroup)
- **PR #4859** — 校对规则支持逐条启用/禁用开关 + 可编辑查找模式(`ProofreadRules` 重写)
- **PR #4948** — OPDS 自动下载目录式目录时爬取子目录(`feedChecker` + `types` 加 `MAX_CRAWL_DEPTH`/`MAX_FEEDS_PER_CRAWL`)
- **PR #5002** — OPDS 自托管目录认证协商 + 自动下载 SSL 修复(`opds/proxy/route` 加 `isPrivateHostAllowed` + `opdsReq` 加 400 重试 + `autoDownload` 加 `skipSslVerification`)
- **PR #4939** — EPUB 元数据中的 Calibre 自定义列(`libs/document` 加 `CalibreCustomColumn` 接口 + `utils/book` 加 `formatCalibreColumnValue` + `libraryUtils` 加 `getCalibreColumnsText` 搜索匹配 + `BookDetailView` 渲染)
- **PR #4427** — 图书馆新增「按阅读进度排序」选项(`types/settings` 加 `Progress` 枚举 + `ViewMenu` 加选项 + `libraryUtils` 加 `getBookReadRatio` 比较)

#### foliate-js 类型声明
- 新增 `src/foliate-js.d.ts` 声明 `getSentences`/`SentenceEntry`/`TTS`(声明为 `any` 类型以匹配上游 allowJs 的宽松推断)
- 解决 Lite Docker 构建(git clone foliate-js)vs 上游(git submodule)的类型推断差异

### Lite 自定义保留(未被覆盖)
- `utils/book.ts` — 保留 v8.12.2 防御性文件名生成,仅新增 `formatCalibreColumnValue`
- `utils/storage.ts` — 保留 'local' 存储类型
- `store/readerStore.ts` — 仅新增 `autoScrollEnabled` + setter
- `services/constants.ts` — 仅新增 `AUTO_SCROLL_*` + `autoScrollSpeed`;移除 `alwaysInForeground` + `showTTSBar`
- `types/book.ts` — 仅新增 `autoScrollSpeed`;移除 `showTTSBar`
- `types/settings.ts` — 仅新增 `Progress` + `autoImportFolders`;移除 `alwaysInForeground`
- `library/page.tsx` — 4 处手术式插入,保留 v8.10 登出守卫 + RemoteDownloadDialog
- `AboutWindow.tsx` — 保留 Lite 品牌(v8.12.1 修复)
- 所有 `pages/api/*` 和 `app/api/*` — 保留 Prisma 版本

### 排除的 PR(不适用 Lite)
- Sentry 崩溃报告(#4914, #4929, #4952, #4958)
- iOS/macOS 原生(#4917, #4891, #4896, #4890, #4909)
- Nix(#4883, #4932)
- Calibre 插件(#4918)
- turso(#4927)
- Android e2e test(#4921)
- koplugin(#4954)
- PR #4949(被 #4989 取代)
- S3 兼容云同步及系列 sync PR(#4990, #4976, #4975, #4982, #4973, #4971, #4981, #4946, #4944, #4928)
- watched folder 自动导入(#4902)— 留待 v8.14

### CI Status
- ✅ Docker Image workflow — build-and-push success
- ✅ CI workflow — Smoke test success
- 镜像已推送:`ghcr.io/cshdotcom/readest-lite:8.13.0` / `8.13` / `latest`

---

## [v8.12.2] — 2026-07-06

### Fixed — 防御性文件名生成 + 更好的上传/下载错误信息

#### 背景
v8.12.1 已修复 v8.12.0 上游同步误覆盖 Lite 自定义代码导致的 "Invalid fileName" 和 "File not found" bug。但修复仍依赖 `getStorageType()` 返回非空字符串,对未来同类回归没有防御。v8.12.2 增加防御性兜底和更好的错误诊断信息。

#### 1. `utils/book.ts` — 防御性文件名生成
- **`getRemoteBookFilename`**:移除对 `getStorageType()` 的分支依赖(Lite 单容器部署不区分 r2/s3/local,统一用可读文件名);兜底:`book.format` 未知 → 用 `.epub`;兜底:`sourceTitle || title || hash` 全空 → 用 `book.hash` 作文件名;永不返回空串或包含空段的路径
- **`getLocalBookFilename`**:同样的防御性兜底
- 移除 `getStorageType` import(不再使用)

#### 2. `pages/api/storage/upload.ts` — 更好的 "Invalid fileName" 错误
- 拒绝时响应体新增 `hint`(校验规则说明)和 `received`(实际收到的值,截断 200 字符)
- 服务端 `console.error` 记录被拒绝的 fileName

#### 3. `pages/api/storage/download.ts` — 更好的 "File not found" 错误 + 放宽 fallback
- 拒绝时响应体新增 `hint`(3 个可能原因)和 `receivedFileKey`(实际查询的 fileKey)
- 服务端 `console.error` 记录未匹配的 fileKey
- **fallback 放宽**:原来要求 `parts.length === 5`,现在按段名找 `Books` 段后读两段(hash + filename),兼容 4 段 cfp 和 5 段 fileKey 以及未来变体

#### 诊断流程
未来再出现 "Invalid fileName" 或 "File not found":
1. 打开浏览器 devtools → Network
2. 找到失败的 `/api/storage/upload` 或 `/api/storage/download` 请求
3. 查看 Response 里的 `hint` 和 `received`/`receivedFileKey` 字段
4. 把这些信息贴到 GitHub issue

### CI Status
- ✅ Docker Image workflow — build-and-push success
- ✅ CI workflow — Smoke test success
- 镜像已推送:`ghcr.io/cshdotcom/readest-lite:8.12.2` / `8.12` / `latest`

---

## [v8.12.1] — 2026-07-03

### Fixed — 紧急修复 v8.12.0 上游同步误覆盖 Lite 自定义代码

v8.12.0 commit message 声称 `utils/storage.ts`、`utils/book.ts`、`AboutWindow.tsx` 是 Lite 自定义文件会保留,但实际 diff 显示这三个文件都被上游版本覆盖,导致两个用户可见的 bug。

#### 1. 「关于」按钮弹出官方页面(AboutWindow.tsx 被覆盖)
- **问题**:AboutWindow.tsx 被替换为上游版本,标题从「About Readest Lite」变成「About Readest」,主标题从「Readest Lite」(带渐变高亮)变成「Readest」(纯文本),版权从「© cshdotcom. Based on Readest.」变成「© Bilingify LLC. All rights reserved.」,源码链接从 `github.com/cshdotcom/readest-lite` 变成 `github.com/readest/readest`,网站链接被删除。
- **修复**:恢复 Lite 品牌定制(标题、主标题、版权、链接),同时**保留** v8.12.0 新增的「Check Update」按钮和 `updateStatus` 状态。

#### 2. 下载书籍时报「File not found」(storage.ts + book.ts 被覆盖)
- **问题根因链**:
  1. `utils/storage.ts` 被覆盖:`ObjectStorageType` 类型从 `'r2'|'s3'|'local'` 缩为 `'r2'|'s3'`,默认从 `'local'` 改为 `'r2'`
  2. `utils/book.ts::getRemoteBookFilename` 被覆盖:上游版本在 storageType 既不是 'r2' 也不是 's3' 时返回 `''`(空字符串)
  3. `runtimeConfig.ts` 仍然把 `objectStorageType` 默认为 `'local'`
  4. 运行时 `getStorageType()` 返回 `'local'` → `getRemoteBookFilename()` 返回 `''`
  5. cfp 变成 `Readest/Books/`(缺文件名段)
  6. fileKey 变成 `<uid>/Readest/Books/`
  7. 下载 API 查 File 表无匹配 → fallback 的 `parts.length === 5` 检查失败(实际 length=4)
  8. 返回 404 "File not found"
- **现象**:文件确实在磁盘上,File 表记录也正确,但客户端构造的 fileKey 缺文件名段,永远匹配不到任何记录。
- **修复**:恢复 `utils/storage.ts` 的 `'local'` 类型和默认值;恢复 `utils/book.ts::getRemoteBookFilename` 的 Lite 分支(`'s3'` → hash-only 文件名,其他包括 `'local'` 和 `'r2'` → 可读文件名)。

### CI 教训
这正是 ITERATION_PROMPT 警告的那种回归:合并上游时,Lite 自定义文件**绝对不能**被覆盖。v8.12.0 commit message 声称保留了,但实际 diff 显示没保留。未来的上游同步**必须**对每个"保留"的文件做 pre-sync/post-sync diff 对比,验证 Lite 特定代码没有丢失。

### CI Status
- ✅ Docker Image workflow — build-and-push success
- ✅ CI workflow — Smoke test success
- 镜像已推送:`ghcr.io/cshdotcom/readest-lite:8.12.1` / `8.12` / `latest`

---

## [v8.12.0] — 2026-07-02

### Added — 上游 v0.11.17 全量同步(逐文件对比)

#### 方法
逐文件对比 Lite 和上游 v0.11.17,分类处理:
- **163 个安全文件**:直接从上游复制(reader 组件/hooks/utils、settings 面板、OPDS、store、services、types、helpers、styles)
- **21 个新文件**:从上游添加(排除 Google Drive/payment/tests)
- **58 个 Lite 自定义文件**:**绝不覆盖**,只手动添加缺失字段

#### Lite 自定义保留(全部完整)
- VaultProvider(AES-GCM 加密)
- PHContext(safe atob,防 SSR 崩溃)
- 本地 JWT 认证(auth pages)
- Prisma + SQLite 后端
- 本地文件存储
- 登出清空逻辑
- 用户/demo 守卫
- syncViewSettings 开关
- Bookshelf group-empty 修复
- deeplink resolveWebBaseUrl
- proxyEnabled + WebDAVSyncLogEntry
- 下载任务(进度条/速度/ETA/日志/批量/Cookie/Headers)
- 阅读统计(总/今日/本周 + 书榜)
- 用户管理折叠
- RemoteDownloadDialog
- 所有 pages/api/* 文件(Supabase → Prisma)

#### 排除(不需要/不适用)
- Google Drive(gdrive provider, gdrive-callback, GoogleDriveForm)
- Stripe/Apple/payment(Plan*, Checkout, useAvailablePlans)
- 所有 __tests__/*

#### 手动添加的类型字段
webtoonMode, showStickyProgressBar, wordLensGlossFontSize/Color, SearchMode, mode,
abandoned(ReadingStatus), coverHash/coverUpdatedAt, fraction(BookProgress),
deletedAt+updatedAt(ProofreadRule), nearbyWords, segments+emphasized+text(SearchExcerpt),
refresh(HardwarePageTurner), biometricUnlockEnabled, WebDAVBrowseSortByType,
browseSortBy/browseSortAscending, synced_at/uploaded_at/downloaded_at(BookDataRecord)

### CI Status
- ✅ TypeScript 通过
- ✅ SSR Collecting page data 通过
- ✅ Docker Image workflow — success
- ✅ CI workflow — Smoke test success
- 镜像已推送:`ghcr.io/cshdotcom/readest-lite:8.12.0` / `8.12` / `latest`

---

## [v8.11.0] — 2026-07-01

### Added — 合并上游 v0.11.13-v0.11.17 功能

#### 1. Markdown (.md) 文件渲染 (#4816)
- 直接打开和阅读 Markdown 文件
- 布局/字体/主题设置与其他格式一致

#### 2. PDF/CBZ 对比度选项 (#4800)
- PDF/CBZ 视图菜单新增对比度调节
- 方便阅读扫描版 PDF

#### 3. TTS 高亮粒度设置 (#4807)
- TTS 朗读时按「单词」或「句子」高亮
- 设置面板中切换

#### 4. 最近阅读书架 (#4829)
- 书库顶部新增「最近阅读」轮播架
- 快速回到上次阅读位置
- 视图菜单可开关

#### 5. 自动翻页角落区域上限 (#4820)
- 自动翻页的角落触发区域 50px 上限
- 避免宽屏误触翻页

#### 6. 清理空高亮 (#4804)
- 取消高亮时如果笔记为空,自动清理占位记录

#### 7. foliate-js 自动更新
- Docker 构建时 `git clone --depth 1` 自动获取最新 foliate-js
- 包含:PDF 滚动模式 pinch-zoom 平滑 (#4817)、PDF 渲染延迟修复 (#4813)、翻页背景重排性能优化 (#4814)

### Not Merged — 双击选词 (#4846)
- 依赖链太深(5+ 个文件 API 修改:useTextSelector、Annotator、SelectionRangeEditor、sel.ts 等)
- cherry-pick 后类型不匹配,已回滚
- 留待未来完整合并

### CI Status
- ✅ Docker Image workflow — success
- ✅ CI workflow — Smoke test success
- 镜像:`ghcr.io/cshdotcom/readest-lite:8.11.0` / `8.11` / `latest`

---

## [v8.10.4] — 2026-06-26

### Added — 恢复跨设备视图设置同步(可选)

#### View Settings 跨设备同步开关
- **背景**:v8.6.0 合并上游 PR #4672 时把 view settings 改成设备本地,导致 A 设备改的字体/主题不会同步到 B 设备
- **实现**:
  - `settings.ts`:新增 `syncViewSettings?: boolean` 字段(默认 `false`)
  - `useProgressSync.ts`:`applyRemoteProgress` 检查 `settings.syncViewSettings`
    - `true`:合并远端 config 到本地(恢复 v8.6 之前行为)
    - `false`:保持 v8.6 设备本地行为(只同步 CFI)
  - `SyncCategoriesSection.tsx`:sync categories 列表下方加 toggle
- **用户操作**:用户中心 → Manage Sync → View Settings toggle

### CI Status
- ✅ Docker Image workflow — success
- ✅ CI workflow — Smoke test success
- 镜像:`ghcr.io/cshdotcom/readest-lite:8.10.4` / `8.10` / `latest`

---

## [v8.10.3] — 2026-06-24

### Fixed — 登出后书籍全部隐藏 + 移出分组不再踢出用户

#### 1. 登出后书籍全部隐藏(修复 demo books 残留)
- **问题**:登出后个别书还显示
- **根因**:`demoBooks` effect 在 `libraryLoaded=true` 时重新添加 demo books。登出时 `handleLogout` 清了 library,但 `initLibrary` 的 `!user` 守卫把 `libraryLoaded` 设为 `true`,`demoBooks` state 还在 → effect 触发 → demo books 加回来
- **修复**:`demoBooks` effect 加 `!token || !user` 守卫,登出后不添加 demo books

#### 2. 移出分组不再踢出用户(修复 group-empty auto-navigate)
- **问题**:用户在分组里把最后一本书移出 → `currentBookshelfItems.length === 0` → Bookshelf 的 effect 自动跳回全部书架,把用户踢出当前分组
- **修复**:只有当 `currentBookshelfItems.length === 0 && selectedGroup` 同时满足且不是用户主动操作时才跳转;用户主动移书不触发跳转

### CI Status
- ✅ Docker Image workflow — success
- ✅ CI workflow — Smoke test success
- 镜像:`ghcr.io/cshdotcom/readest-lite:8.10.3` / `8.10` / `latest`

---

## [v8.10.2] — 2026-06-24

### Fixed — 笔记导出链接绝对 URL + 用户管理折叠

#### 1. 笔记导出链接打不开(核心 bug)
- **问题**:导出笔记后,点击笔记里引用原书的链接,浏览器报「无法打开此书籍」
- **根因**:`buildAnnotationWebUrl` 用构建期常量 `READEST_WEB_BASE_URL`(在 Lite 里硬编码为 `''`),导出的链接是相对路径 `/o/book/{hash}/annotation/{id}`。用户在站外(GitHub/Obsidian/VS Code)点开相对路径,浏览器不知道指向哪个域名
- **修复**:新增 `resolveWebBaseUrl()` 运行时解析:
  1. 浏览器运行时:`runtimeConfig.apiBaseUrl`(`PUBLIC_BASE_URL` 注入的完整 URL,反代场景)
  2. 浏览器回退:`window.location.origin`(用户直接 IP:端口访问,没设 `PUBLIC_BASE_URL`)
  3. 服务端:构建期常量(SSR 场景,不会真正用于导出)
- **影响文件**:`utils/webUrl.ts`、`apps/readest-app/src/app/o/page.tsx`、`apps/readest-app/src/app/o/book/[hash]/annotation/[id]/page.tsx`

#### 2. Reader 书不在库里时自动重试加载
- **问题**:从笔记链接点回 reader,如果书不在本地 library,会停留在空白页
- **修复**:`useOpenAnnotationLink.ts` 检测到书不在 library 时,先 toast 提示「正在加载...」,然后导航到书库页让用户手动同步,而不是只 toast 就放弃

#### 3. 用户管理折叠
- **问题**:用户数量多时用户中心面板很长
- **修复**:用户管理列表默认折叠,点击「展开 (N)」按钮才展开

### CI Status
- ✅ Docker Image workflow — success
- ✅ CI workflow — Smoke test success
- 镜像:`ghcr.io/cshdotcom/readest-lite:8.10.2` / `8.10` / `latest`

---

## [v8.10.1] — 2026-06-23

### Added — 批量下载 per-URL Cookie/Headers 语法

#### 批量下载支持每行 URL 单独配 Cookie/Headers
- **问题**:v8.9 的批量下载只支持一组全局 Cookie/Headers 应用到所有 URL。如果 URL 来自不同认证网站,用户要手动分多次提交
- **解决**:扩展 textarea 语法,支持 per-URL 指令:

```
# 开头的行是注释
https://example.com/free.epub

https://site-a.com/book1.epub | cookie:sessionid=abc123

https://site-b.com/book2.epub | cookie:PHPSESSID=def | header:Referer: https://site-b.com
```

#### 语法规则
| 指令 | 格式 | 示例 |
|---|---|---|
| Cookie | `cookie:VALUE` | `cookie:sessionid=abc123; theme=dark` |
| Header | `header:Key: VALUE` | `header:Referer: https://site-b.com` |
| 多指令 | ` \| ` 分隔 | `cookie:abc \| header:Referer: https://x.com` |
| 注释 | `#` 开头 | `# 这是注释` |
| 空行 | 忽略 | — |

### CI Status
- ✅ Docker Image workflow — success
- ✅ CI workflow — Smoke test success
- 镜像:`ghcr.io/cshdotcom/readest-lite:8.10.1` / `8.10` / `latest`

---

## [v8.10.0] — 2026-06-23

### Added — 中文汉化 + 阅读统计 + 下载折叠

#### 1. 中文汉化(zh-CN + zh-TW)
- 补全 v8.7-v8.9 所有新增字符串的中文翻译(60 个 key)
- 包括:下载任务、进度条、日志、批量、高级选项、Cookie/Headers 等
- 阅读统计相关新字符串一并加入

#### 2. 阅读统计功能
- **新建 `GET /api/stats/aggregate` 端点**
  - 返回 `total` / `today` / `week` 聚合 + `books` 排行榜
  - 数据源:`StatPage` 表(每条记录是一次 page-read 事件)
  - 时间窗口:今日 0:00 / 本周周一 0:00 / 全部
- **`ReadingStatsCard.tsx`(用户中心顶部)**
  - 横向滚动卡片:总时间 / 今日 / 本周 / 日均
  - 前 3 本书迷你榜
  - 点击打开 `ReadingStatsModal`
  - 30 秒轮询
- **`ReadingStatsModal.tsx`**
  - Tab: 今日 / 本周 / 总计
  - 每个 Tab 显示总计 / 书籍数 / 日均
  - 书籍排行榜:按阅读时间排序(高到低/低到高切换)
  - 渐变进度条(根据阅读进度变色:红→黄→蓝→绿)
  - 搜索框筛选书籍

#### 3. 下载记录折叠
- `DownloadTasks.tsx` 默认只显示前 3 条
  超过 3 条时显示「查看全部 (N)」按钮
- 新建 `DownloadTasksModal.tsx`:完整列表,可滚动
  复用单条任务的渲染逻辑

### Fixed — 笔记链接手机 + 登出安全

#### 4. 笔记导出链接手机修复
- `/o/page.tsx`:手机(Android/iOS)默认直接跳 web reader,不再尝试启动 App
  - 原因:手机没装 App 时 `readest://` scheme 会触发「无法打开页面」错误
  - 桌面仍然尝试启动 App(桌面浏览器优雅处理 unknown scheme)
- `useOpenAnnotationLink.ts`:书不在库里时不要只弹 toast 就放弃
  改为提示用户并导航到书库页,避免停留在空白 reader

#### 5. 登出后残留书籍修复
- `useUserActions.handleLogout`:
  - 新增 `appService.saveLibraryBooks([])` 清空白名单明文 `library.json`
    防止登出后 `loadLibraryBooks` 走明文路径读到旧书
  - 新增 `useLibraryStore.setState({ libraryLoaded: false })`
    让下次登录时重新从磁盘加载
- `library/page.tsx`:`initLibrary` 加 `user/token` 守卫
  未登录时跳过 `loadLibraryBooks`,避免 vault 清空后走明文路径

### CI Status
- ✅ Docker Image workflow — `build-and-push` success
- ✅ CI workflow — `Build Docker image` + `Smoke test` 全部通过
- 镜像已推送:`ghcr.io/cshdotcom/readest-lite:8.10.0` / `8.10` / `latest`

## [v8.9.0] — 2026-06-22

### Added — 下载任务增强:进度/速度/ETA/日志/批量/自动重命名/Cookie

#### 1. Prisma schema 扩展

`DownloadTask` 表新增字段:
- `progress` (Int 0-100) — 实时下载百分比
- `downloadedBytes` (BigInt) — 已下载字节数
- `totalBytes` (BigInt?) — 总字节数(从 Content-Length)
- `speedBps` (Int) — 当前速度(字节/秒,5 秒滑动窗口)
- `etaSeconds` (Int?) — 预计剩余秒数
- `cookies` (String?) — 用户提供的 Cookie 头
- `customHeaders` (String?) — JSON 序列化的自定义 headers
- `originalUrl` / `originalFilename` — auto-rename 前的值(用于 UI 显示)

新建 `DownloadLog` 表:
- `id`, `taskId`, `level` (info|warn|error), `message`, `createdAt`
- `@@index([taskId, createdAt])` — 按任务查日志的索引
- `onDelete: Cascade` — 删 task 自动删日志

#### 2. `filenameDetect.ts` — 智能文件名识别

优先级:`Content-Disposition` > URL path > URL query `file=` > base64 decode > Content-Type > fallback

支持场景:
- 直接明文 URL: `https://example.com/book.epub` → `book.epub`
- URL 编码中文: `%E4%B8%AD%E6%96%87.epub` → `中文.epub`
- 带查询参数: `book.epub?file=abc` → `book.epub` (剥离 ?)
- URL query `?file=book.epub` → `book.epub` (从 query 提取)
- Base64 编码: `Zm9vYmFyLmVwdWI=` → `foobar.epub` (尝试解码)
- 完全无扩展名: 用 Content-Type 推断 .epub/.pdf/.mobi 等
- 乱码 fallback: `book-<timestamp>.epub`

#### 3. `downloadRunner.ts` — 共享下载执行器

被 `POST create` / `retry` / `batch retry_failed` / `batch resume_all` 共用:

- 流式读取 `response.body.getReader()`,实时统计字节数
- **每秒 throttle 写库**(progress / downloadedBytes / totalBytes / speedBps / etaSeconds)
- 速度算法:最近 5 秒滑动窗口样本平均
- ETA:`(totalBytes - downloadedBytes) / speedBps`
- **每 2 秒独立检查暂停状态**(不被进度更新干扰,确保快速响应暂停)
- 完整日志写入 `DownloadLog` 表(info / warn / error 三级)
- 支持 `cookies` + `customHeaders` 注入到 fetch headers
- 用 `filenameDetect` 在收到响应后智能识别文件名
- 完成后写 `File` + `Book` 表,更新 task 状态为 `completed`

#### 4. API 路由

- `GET /api/download-tasks` — 返回 progress / speed / eta / hasCookies / hasCustomHeaders
- `POST /api/download-tasks` — body 加 `cookies` / `headers` / `batch` 字段
  - `batch: string[]` → 批量创建任务
- `POST /api/download-tasks/[id]` — `retry` 调用 `runDownloadTask`
- `POST /api/download-tasks/batch` — 新增 `action=create` 支持 batch URL 提交
- `GET /api/download-tasks/[id]/logs` — **新端点**,返回任务完整日志
  - 支持 `?level=info|warn|error` & `limit=N` & `offset=N`

#### 5. `RemoteDownloadDialog` 重写 — 单任务 + 批量 + 高级选项

- Tab 切换: Single | Batch
- Single: URL + 可选 filename(提示自动检测)
- Batch: textarea 一行一个 URL,最多 20 个,实时计数
- **Advanced Options 折叠区**(单任务和批量都有):
  - Cookies textarea(格式:`key1=val1; key2=val2`)
  - Custom Headers 列表(key-value 行,可增删)
  - 说明文字提示类似 `curl -H`

#### 6. `DownloadTasks.tsx` 重写 — 进度条 + 速度 + ETA + 用时

每行任务显示:
- 状态图标 + 文件名 + status badge
- **progress bar** (in_progress / paused / completed)
- `downloadedBytes / totalBytes` + 百分比
- **速度** (B/s, KB/s, MB/s) + **ETA** (5s, 2m30s, 1h5m)
- URL(点击复制)+ 创建时间 + **已用时**
- auto-renamed / cookie / headers badge
- 点击任务行 → 打开 `DownloadTaskDetailModal`
- 3 秒轮询任务列表(有 pending/in_progress 时)
- 1 秒 tick 重渲染刷新用时显示

#### 7. `DownloadTaskDetailModal.tsx` — 任务详情 Modal

- 显示完整日志(info / warn / error 三色)
- 筛选: All / INFO / WARN / ERROR
- Auto-scroll 开关(默认开启)
- 任务元信息: status / 原文件名 → 新文件名 / Cookie / Headers
- 2 秒轮询日志 + 任务状态
- 用 `useRef` + `useEffect deps=[task?.status]` 避免无限重渲染

### Fixed — v8.9.0 CI 稳定化

- `94cc02c` `filenameDetect.ts` `noUncheckedIndexedAccess` 修复
  - `starMatch[1]` → 加 `starMatch && starMatch[1]` 守卫
  - `plainMatch[1]` → 加 `plainMatch && plainMatch[1]` 守卫
  - `split(';')[0]` → 用中间变量 + `|| ''` 兜底

### CI Status
- ✅ Docker Image workflow — `build-and-push` success
- ✅ CI workflow — `Build Docker image` + `Smoke test` 全部通过
- 镜像已推送:`ghcr.io/cshdotcom/readest-lite:8.9.0` / `8.9` / `latest`

## [v8.8.0] — 2026-06-21

### Added — 分块上传规避 Cloudflare 524 超时
- `apps/readest-app/src/utils/localStorage.ts`
  - `createPartWriteStream(fileKey, index, total)` — 写第 N 块到
    `<fileKey>.parts/<NNNNN>`(5 位补零,确保字典序 == 数字序)。
    当 `index === 0` 时先清空 parts 目录,避免上次失败上传的残留 part
    干扰本次 merge 校验。
  - `mergePartsForKey(fileKey, expectedTotal)` — 校验 part 数量 + 名称
    后,用 `Readable.from(async generator)` + `stream/promises.pipeline`
    流式合并所有 parts 到 `<fileKey>`(不一次性 buffer 整个大文件到内存),
    最后删除 `.parts` 目录。
- `apps/readest-app/src/pages/api/storage/_put.ts` — 三个分支
  - `merge=1&total=M` → 调 `mergePartsForKey` 触发流式合并
  - `index=N&total=M` → 调 `createPartWriteStream` 写第 N 块
  - 无额外参数 → 旧的整文件直传路径(小文件 + Tauri 客户端)

### Changed — webUpload 自动分块
- `apps/readest-app/src/utils/transfer.ts` 的 `webUpload`
  - 文件 <= 5MB:单次 PUT(旧路径,零行为变化)
  - 文件  > 5MB:切成 5MB 块,串行 PUT 每块到
    `/api/storage/_put?...&index=N&total=M`,
    最后再发一次 `PUT &merge=1&total=M` 触发服务端合并
  - 进度回调跨块累计 `progress` / `total`,UI 显示连续进度条
  - URL 解析用 `window.location.href` 作 base,兼容绝对 URL
    (`PUBLIC_BASE_URL` 反代场景)和相对 URL(本地直连场景)

### Fixed — Cloudflare 反代下大文件上传 524 超时
- **问题**:用户走 Cloudflare 反代访问时,大文件(>50MB)整传超 100 秒
  触发 CF 524 状态码,上传中断。浏览器控制台报:
  `Failed to load resource: the server responded with a status of 524`
  `File upload failed: Error: Upload failed with status 524`
- **根因**:CF 默认 100 秒硬性 origin response timeout,源服务器在
  接收上传期间不发响应,超时即断。
- **修复**:5MB 块在慢带宽下也能在 ~30 秒内传完,远低于 100 秒限制。
  服务端流式合并在 SSD 上 ~5-10 秒/GB,HDD ~30-60 秒/GB,也不超时。

### Backward Compatibility
- 小文件(<=5MB)走原直传路径,行为完全不变
- Tauri 客户端用 `tauriUpload`(不是 `webUpload`),不受影响
- 旧版客户端继续向新服务端整文件 PUT,依然可用(_put.ts 第 3 分支)
- 新版客户端向旧服务端发 `&index=` 参数会被忽略走整传路径 — 但实际不会
  发生,因为新客户端只 PUT 文件本体,分块参数只在 webUpload 内部加

### CI Status
- ✅ Docker Image workflow — `build-and-push` success
- ✅ CI workflow — `Build Docker image` + `Smoke test` 全部通过
- 镜像已推送:`ghcr.io/cshdotcom/readest-lite:8.8.0` / `8.8` / `latest`

## [v8.7.0] — 2026-06-21

### Added — 跨设备下载任务队列
- `prisma/schema.prisma` — 新增 `DownloadTask` 表(id, userId, url, filename,
  status, error, bookHash, fileSize, createdAt, startedAt, completedAt)
  支持跨设备同步的远程下载任务队列
- `apps/readest-app/src/app/api/download-tasks/route.ts`
  - `GET /api/download-tasks` — 列出当前用户所有任务
  - `POST /api/download-tasks` — 创建任务(异步下载,后台 fetch → 写 File +
    Book 表 → 更新任务状态)
- `apps/readest-app/src/app/api/download-tasks/[id]/route.ts`
  - `DELETE /api/download-tasks/[id]` — 删除单个任务
  - `POST /api/download-tasks/[id]` — 重试 / 暂停 / 恢复(body: `{ action }`)
- `apps/readest-app/src/app/api/download-tasks/batch/route.ts`
  - `POST /api/download-tasks/batch` — 批量操作(retry_failed / pause_all /
    resume_all / clear_completed / clear_failed / clear_all)
- `apps/readest-app/src/app/user/components/DownloadTasks.tsx` — 用户中心
  新增下载任务面板:5s 轮询、状态图标、批量按钮、单条重试/暂停/恢复/删除、
  URL 一键复制
- `apps/readest-app/src/app/user/page.tsx` — 所有用户(不止 admin)可见
  DownloadTasks 面板

### Changed
- `RemoteDownloadDialog.tsx` 简化:POST 创建任务后 toast 提示去用户中心
  查看进度,不再前端 transferStore 跟踪(任务状态已落库,跨设备可见)
- `library/page.tsx` — `refresh-library` 事件改用 `useCallback` 稳定引用,
  确保 `eventDispatcher.off()` 能正确解绑(修复 v8.7.0 CI 失败:
  `Expected 2 arguments, but got 1`)

### Fixed — v8.7.0 CI 稳定化(3 个 follow-up commit)
- `78c0deb` 移除 `[id]/route.ts` 中未使用的 `ALLOWED_EXTENSIONS`
  常量(触发 TS `noUnusedLocals`)
- `78c0deb` 移除 `DownloadTasks.tsx` 中未使用的 `IoAlertCircleOutline`
  import(同样触发 `noUnusedLocals`)
- `e43a3a0` `eventDispatcher.off('refresh-library', handleRefreshLibrary)`
  改为传 2 个参数(API 签名要求 event + callback)

### CI Status
- ✅ Docker Image workflow — `build-and-push` 成功,镜像已推送:
  `ghcr.io/cshdotcom/readest-lite:8.7.0` / `8.7` / `sha-e43a3a0` / `latest`
- ✅ CI workflow — `Build Docker image` + `Smoke test — container starts and
  auth works` 均通过

## [0.1.0] — 2026-06-18

### Added — backend infrastructure
- `prisma/schema.prisma` — 14 tables fully aligned with original Supabase schema
  (User, Book, BookConfig, BookNote, File, BookShare, ReplicaKey, Replica,
  SendAddress, SendAllowedSender, SendInbox, StatBook, StatPage, UsageStat)
- `utils/db.ts` — Prisma client singleton
- `utils/localAuth.ts` — JWT (HS256) sign/verify + email/password login +
  admin user initialization (UUID v5 from ADMIN_EMAIL)
- `utils/localStorage.ts` — local filesystem storage with HMAC-SHA256 signed
  PUT/GET URLs (drop-in replacement for R2/S3 presigned URLs)
- `utils/crdt.ts` — CRDT merge functions reimplemented in TypeScript
  (hlcMax, crdtMergeFields, crdtComputeUpdatedAt, crdtMergeReplica,
  stripCipherEnvelopes) — equivalent to Postgres PL/pgSQL RPCs
  `crdt_merge_replica`, `crdt_merge_fields`, `crdt_compute_updated_at`,
  `hlc_max`, `replica_keys_forget`
- `utils/supabase.ts` — pseudo `@supabase/supabase-js` client that routes
  auth calls to local `/auth/v1/*` (zero frontend changes)
- `utils/access.ts` — `validateUserAndToken` rewritten to verify local JWT;
  all plan/quota helpers return unlimited (Pro system removed)
- `utils/object.ts` — unified facade over `localStorage`
- `utils/usage.ts` — translation usage stats backed by SQLite `UsageStat` table
  (replaces Supabase RPCs `increment_daily_usage` / `get_current_usage`)
- `app/api/auth/[...path]/route.ts` — Supabase Auth v1 compatibility shim
  implementing `signup` (403), `token?grant_type=password`,
  `token?grant_type=refresh_token`, `user`, `logout`, `magiclink` (403),
  `recover` (403), `reset` (403), `verify` (403), `settings`
- `pages/api/storage/_put.ts` — internal PUT endpoint receiving client
  direct-upload bytes (HMAC signature verified, streams to local file)
- `pages/api/storage/_get.ts` — internal GET endpoint streaming local file
  with HTTP Range support (HMAC signature verified)
- `apps/readest-app/scripts/init-admin.ts` — startup script that creates or
  updates the admin user from `ADMIN_EMAIL`/`ADMIN_PASSWORD` env vars
- `docker/entrypoint.sh` — startup script: `prisma db push` + `init-admin` +
  `node server.js`
- `.env.example` — full env var template
- `Dockerfile` — multi-stage build (dependencies → build → production)
- `docker-compose.yml` — single-container orchestration with `/data` volume
- `DEPLOY.md` — full deployment + verification guide
- `FRONTEND_CHANGES.md` — line-precise diff of frontend deletions
- `PROJECT_STRUCTURE.md` — directory layout after refactor
- `.github/workflows/ci.yml` — CI: install + Prisma generate + build-web + Docker build
- `.github/workflows/docker-image.yml` — build and push Docker image to GHCR

### Replaced — backend API routes (supabase → prisma)
- `pages/api/sync.ts` — main sync API (GET/POST) with last-writer-wins +
  soft-delete union + stat_pages pickWinningPages
- `pages/api/sync/replicas.ts` — CRDT replica sync (GET/POST) with
  `crdtMergeReplica` per-row merge
- `pages/api/sync/replica-keys.ts` — PBKDF2-600k-SHA256 salt list/create/forget
- `pages/api/storage/{upload,download,list,delete,purge,stats}.ts` —
  all storage endpoints; quota enforcement removed (unlimited)
- `pages/api/send/{address,senders,inbox,inbox/claim,inbox/file,
  inbox/[id]/payload,inbox/[id]/transition,fetch-url}.ts` —
  all Send-to-Readest endpoints; `claim` uses optimistic locking instead of
  `FOR UPDATE SKIP LOCKED`
- `pages/api/user/delete.ts` — disabled (protects the only admin)
- `pages/api/deepl/translate.ts` — usage stats written to SQLite `UsageStat`
- `app/api/share/{create,list,[token],[token]/download,[token]/download/confirm,
  [token]/import,[token]/revoke,[token]/cover,[token]/og.png}.ts` —
  all share endpoints with identical paths/status codes/error codes
- `libs/shareServer.ts` — `resolveActiveShare` + `rejectionToHttp` rewritten
  with Prisma

### Replaced — frontend
- `app/auth/page.tsx` — reduced from 454 lines (OAuth + Magic Link +
  Apple Sign-In + social login) to ~100 lines (email/password only)
- `hooks/useQuotaStats.ts` — returns unlimited quotas
- `services/constants.ts` — `DEFAULT_STORAGE_QUOTA` and
  `DEFAULT_DAILY_TRANSLATION_QUOTA` set to `Number.MAX_SAFE_INTEGER`
- `app/library/components/SettingsMenu.tsx` — removed "Upgrade to Readest
  Premium" menu item (lines 441-444) and `handleUpgrade` function
- `components/settings/integrations/SendToReadestForm.tsx` — removed
  `userPlan` state, `canUseEmailIn` gate, and the entire upgrade card
  UI block (lines 226-253); removed `getUserProfilePlan`/`isEmailInPlan`
  imports
- `app/user/page.tsx` — completely rewritten: removed `PlansComparison`,
  `Checkout`, all Stripe/IAP handlers, `useAvailablePlans` hook;
  kept account info, usage stats, account actions, storage manager,
  shared links, sync settings
- `app/user/components/UserInfo.tsx` — `planDetails` now optional
  (renders no badge when null)
- `services/translators/providers/deepl.ts` — removed `getSubscriptionPlan`/
  `getTranslationQuota` imports and plan-gated quota logic
- `hooks/useTranslator.ts` — replaced "Upgrade your plan" toast message with
  generic "Please try again later"

### Deleted — backend
- `app/api/stripe/{check,checkout,plans,portal,webhook}/` — 5 Stripe routes
- `app/api/apple/iap-verify/` — Apple IAP verification
- `app/api/google/iap-verify/` — Google IAP verification
- `libs/payment/` — entire payment library (Stripe + IAP + storage helper)

### Deleted — frontend
- `app/user/components/{PlanActionButton,PlanCard,PlanIndicators,
  PlanNavigation,PlansComparison,PurchaseCallToActions,Checkout}.tsx` —
  7 Pro UI components
- `app/user/utils/plan.ts` — plan details helper
- `app/user/subscription/` — subscription success page
- `hooks/useAvailablePlans.ts` — Stripe/IAP plan fetcher
- `types/payment.ts` — payment type definitions
- `app/auth/utils/` — Tauri-specific OAuth helpers (no longer used)

### Deleted — non-web infrastructure (out of scope for single-container)
- `apps/readest-app/src-tauri/` — Tauri native shell (desktop/mobile)
- `apps/readest.koplugin/` — KOReader plugin
- `fastlane/` — mobile release pipeline
- `data/`, `apps/readest-app/data/` — screenshots and metadata
- `apps/readest-app/{e2e,bench,workers,extensions,docs}/` — test/bench/worker
  code not needed for the lite build
- `apps/readest-app/src/__tests__/` — Vitest test suite (would require
  significant updates; CI runs build only)
- `.github/workflows/{android-e2e,nightly,release,scorecard,upload-to-r2,
  vercel-merge}.yml` — CI for upstream release infrastructure

### Preserved — untouched
- All reader core (foliate-js, pdfjs, simplecc-wasm, jieba-wasm)
- All client sync logic (`services/sync/*`, `libs/sync.ts`,
  `libs/replicaSyncClient.ts`, `libs/replicaSyncServer.ts`,
  `libs/replicaSchemas.ts`, `libs/crdt.ts`)
- All client share logic (`libs/share.ts`, `libs/shareImport.ts`)
- All client transfer logic (`services/transferManager.ts`, `utils/transfer.ts`)
- All transparent proxy routes (`kosync.ts`, `app/api/{ai,tts,metadata,opds,
  hardcover}/`) — only `validateUserAndToken` swapped to local impl
- `context/AuthContext.tsx` — works as-is via pseudo supabase client
- `helpers/auth.ts` — works as-is via pseudo supabase client
- `app/auth/{callback,error,recovery,update}/` — works as-is
- All UI components (except the 7 Pro components deleted)
- `middleware.ts` — CORS/COEP unchanged
- `next.config.mjs` — unchanged
- `services/send/sendAddress.ts` — address generation logic unchanged
- All Tauri-side `services/{nativeAppService,nodeAppService}.ts` (kept for
  type compatibility; not invoked in web-only build)

### Key contracts preserved (1:1 with upstream)
- `/api/sync` GET/POST — request shape, response shape, last-writer-wins,
  soft-delete union, stat_pages duration-wins, books.progress piggyback
- `/api/sync/replicas` GET/POST — HLC-based cursor, `cursors` batch pull,
  `rows` push, CRDT merge semantics (remove-wins, deviceId tiebreak,
  reincarnation, manifest null-preservation)
- `/api/sync/replica-keys` GET/POST/DELETE — `pbkdf2-600k-sha256` only,
  32-byte random salt, base64 wire format, cipher envelope stripping on forget
- `/api/storage/*` — `uploadUrl`/`downloadUrl`/`fileKey`/`usage`/`quota`
  response fields; `file_key` naming `${userId}/Readest/Books/<hash>.<ext>`;
  presigned URL TTL 1800s; temp upload TTL 3 days; `isSafeObjectKeyName`
  traversal protection
- `/api/share/*` — paths, status codes (400/404/409/410/429), error codes
  (`invalid_token`/`not_found`/`revoked`/`expired`/`source_deleted`/
  `book_not_uploaded`/`upload_incomplete`/`share_limit_reached`/
  `quota_exceeded`), `SHARE_MAX_PER_USER=50`, atomic `download_count`
  increment via conditional update
- `/auth/v1/*` — supabase-js compatible paths and response shapes;
  JWT HS256 with `sub`/`aud`/`exp`/`email`/`plan`/`storage_usage_bytes`/
  `storage_purchased_bytes` claims
- `runtime-config.js` — still returns `supabaseUrl`/`supabaseAnonKey`/
  `apiBaseUrl`/`objectStorageType`/`storageFixedQuota`/
  `translationFixedQuota` (frontend boot expects these)

### Operational notes
- Single Docker image, single container, port 8225
- Data volume `/data` (SQLite db at `/data/db/readest.db`, books at
  `/data/books/`, inbox at `/data/inbox/`)
- Container restart auto-runs `prisma db push` (schema sync) and
  `init-admin` (idempotent admin creation/password sync)
- JWT secret rotates via env var `JWT_SECRET`; access token TTL 7 days,
  refresh token TTL 30 days (configurable)
- SQLite WAL mode enabled by Prisma for concurrent read/write
- No file count or size limits (Pro system removed)