# 私聊、群聊与主题的共享日期导航

## 交互

- 私聊、群聊在对话右上角提供日历图标，与搜索、通话并排，复用现有标题栏按钮。悬浮提示「按日期查看聊天记录 · N 个日期」；N 是有消息的日期数，不是会话存续天数。输入框上方不再保留入口。
- 发给自己和主题保留原来的日历入口，与聊天共用多月滚动日历、日期格、今日标记和定位状态。
- 星期固定，月份纵向滚动；会话日历不再显示独立的顶部年月和翻月箭头，点击正文月份标题即可输入年月跳到往年。每次打开会话日历，展示并高亮当前阅读日期；跨天时取顶部第一条可见消息，滚到最底部时取最后一条消息。复用既有阅读位置锚点，仅在打开时读取，不随每次滚动重渲染或增加网络读取。
- 浮层默认高度 760px，尽量显示纵向双月，不缩小日期尺寸；受窗口可用空间限制。底边提供轻量拖动条，向下增加、向上减少，默认最小高度 360px；支持方向键和 Home/End。高度在同一账号、本机浏览器来源内由私聊、群聊、发给自己及主题共用并持久保存。窗口变小时只限制实际尺寸，不覆盖保存高度；取消拖动不保存。第一栏独立日历不受影响。
- 读取不到消息位置时，聊天退回最近有消息的日期。晚返回的统计不得覆盖已知阅读日期，也不打断手动翻月。翻日历本身不移动消息；定位失败或取消后重新打开，仍以实际阅读位置为准。
- 阅读日期、视口月份与可滚动范围分别维护。打开历史月份时包含相邻的更新月份，上下接近边缘时分批追加，最晚到当前月，不能把当前阅读月份当成数据终点。远距离月份跳转使用小范围窗口并继续支持双向延伸。前插月份保留视口，后追加不改变滚动位置；拉高浮层时按需补充月份，发给自己和主题仍按可见月份加载并复用缓存。
- 对话日历移除「回到今日」，今天保留轻量描边而不抢占选中状态。返回最新消息沿用会话现有入口；第一栏独立日历仍保留「回到今日」。
- 选择日期立即关闭日历，在会话内显示定位中状态。完成后从当天首条目标消息开始显示并高亮，避免居中定位露出前一天、重新打开日历又选中前一天。已加载消息和新加载历史窗口共用定位算法；搜索等其他消息定位仍居中。支持取消、重试、切换会话取消旧请求。

## 已有接口复用

Flutter `ChatPhasedRequestClient.fetchChatSessionStatistics` 使用：

`POST /api/v1/chats/session/statistics`

参数：`chat_session_uid`、`tz_offset_millis`。桌面通过签名 `sourceRef` 解析当前用户有权访问的会话，不接受前端任意指定原始会话 ID。

返回 `daily_data[]` 的 `bucket_date`、`count`、`first_record_uid`、`first_record_owner_user_id`、`first_attach_at`。桌面使用同样的时区偏移与日期口径；定位时间使用消息加入会话的时间，而非转发前原记录的创建时间。

`bucket_date` 兼容生产实际返回的 `YYYYMMDD` 和 `YYYY-MM-DD`，统一为内部的 `YYYY-MM-DD`。缺失或无效时，按 Flutter 的兼容策略使用 `day_milli_stamp`，结合请求客户端的 `tz_offset_millis` 转为日期，不使用服务端进程所在时区。两者均无效时明确报错，不能缓存成空日历。

选择日期后直接调用已有的 `/api/v1/chat/timeline/around`，保留完整 owner ID，获取目标消息前后内容。没有逐页扫描全部聊天记录，也不借用个人日历推算群聊历史。

发给自己、未分类和主题使用 Record 新视图日历接口，过滤、可见性和子主题去重由后端统一完成，不再逐日拉取正文重算数量。月份只请求 `buckets/query`，点击日期请求 `records/query` 的 `oldest_first=true, limit=1` 第一页再定位。完整对接说明见 [发给自己日历接口升级](self-calendar-view-integration.md)。Flutter 主题的 `/api/v1/topics/display/statistics` 是另一种统计范围，未直接替换。

## 缓存与边界

- 私聊日期现在合并现有 `source.interwoven-moments` 的已授权互动卡片，按 moment ID 去重、按会话统计的时区偏移分桶；群聊和发给自己不受此合并影响。合并结果只在客户端投影，不污染消息统计缓存，不重复请求群聊历史。
- 日期格保留总数，悬浮仅显示非零分类（如“群聊互动 2 条”或“私聊 3 条 · 群聊互动 2 条”）；纯互动日期也可以点击。首次互动加载中、部分失败、失败重试均明确标注，当前 bootstrap 缺少权威完整覆盖契约，所以私聊提示“已知日期/已加载互动”，不把未知日期说成无内容。
- 私聊定位以当天最早消息或互动为目标；互动定位留在当前私聊，不把群消息 ID 传入私聊 around 接口。已渲染目标直接定位，较早卡片通过现有私聊锚点补有界上下文，并显式纳入内联时间轴。读取日期同时识别消息与互动，过滤系统提示。
- 完整历史日统计、混合时间轴精确定位和版本增量需求见 [后端需求](private-calendar-interaction-backend-requirements.md)。

- 前端复用 `CalendarMonthCache`，按账号、会话、时区及偏移隔离，缓存摘要与定位标识，不保存正文或媒体地址。
- 首次进入读取服务端日期索引；再次打开优先使用缓存，超过 60 秒后台刷新，重启后的缓存也会重新验证。
- 消息变化沿用现有日历失效通知，刷新会话摘要。现有接口没有增量游标，因此刷新的是日期摘要，不宣称服务端增量更新。
- 后端请求合并并发读取，短期缓存；账号变化、权限失败或硬失效不继续显示旧账号/旧权限数据。
- 接口失败不展示「0 个日期」；无定位 owner 的旧统计不会猜测归属者，也不会退回全量翻页扫描。

## 验证

服务契约、账号/会话隔离、缓存恢复、多月渲染、精确 owner、定位成功/取消/失败/切换会话均有自动化测试。
2026-09-18 在已登录的本地预览完成真实接口与界面验收：群聊显示 83 个有消息日期，私聊显示 136 个有消息日期；分别点击 7 月 1 日、7 月 30 日后显示定位中状态并进入相应历史消息。入口位于标题栏，输入框上方无重复入口，日历无日期格式报错。自动化另覆盖标准/紧凑日期、时间戳兜底、时区跨日与无效数据重试。

同日完成阅读位置联动验收：群聊定位到 7 月 1 日、私聊定位到 7 月 30 日、发给自己定位到 7 月 1 日后，重新打开日历均选中实际定位日期；只浏览其他月份后重新打开，仍回到实际阅读日期。对话日历无「回到今日」。12 个相关测试文件、415 项测试通过，另覆盖主题入口、跨日视口、底部日期、晚返回统计、定位未完成时重开及独立日历保留「回到今日」；构建通过。

随后修正历史月份被当成滚动终点的问题：实际在发给自己定位 8 月 12 日并重开日历，7/8/9 月连续存在；向下滚动直接看到 9 月记录，顶部月份跟随为 9 月，阅读日期仍为 8 月 12 日。向上滚动自动补充更早月份，9 月仍保留；关闭重开仍定位 8 月 12 日。13 个相关测试文件、423 项测试通过，新增双向延伸、标题跟随、视口保持、跨年直到当前月、远距离跳月及发给自己/主题按需加载与缓存复用覆盖；类型检查、构建通过。

同日新增私聊互动日历验收：真实预览中 9 月 4、5、15、16、17 日的纯互动日期已可点击，其中 9 月 16 日为 2 条互动；9 月 18 日为 2 条私聊 + 1 条互动。选择 8 月 4 日先显示定位中，再定位到原私聊内当天第一张互动卡片；重新打开日历选中 8 月 4 日。切换到其他会话再返回后，仍恢复到同一张历史互动卡片，日历保持选中 8 月 4 日。20 个相关测试文件、544 项测试通过，包含互动去重/跨日分桶、同日首项选择、纯互动日、数据不完整提示、缓存隔离，以及真实 Sidebar 定位完成/取消/失败/切换会话与原有消息搜索定位回归。这不代表服务端完整历史互动统计已上线。

同日完成日历高度与精简布局验收：21 个相关测试文件、559 项测试通过，类型检查及构建通过。真实预览去除重复顶部导航后可同时展示 8、9 月；底边完整拖动、松手保存、关闭重开及页面刷新后恢复高度均通过（963px 调回 760px）。发给自己复用同一高度，月份标题仍支持快速跳转。纯互动日期提示为「群聊互动 2 条」，混合日期为「私聊 2 条 · 群聊互动 1 条」，不显示零分类。自动化额外覆盖账户隔离、小窗口限制但不覆盖偏好、拖动取消/丢失捕获、键盘调高、重新挂载及存储不可用；未实测原生客户端重启。
