Commit Graph
8 Commits
Author SHA1 Message Date
laoyang a5e559b2b3 v25 第 25 批(9 条):名册分组不刷页 / 纪念日多人 / 首页等高 / 移出不回流 /
名册点击看详情拖动才排序 / 任务复选框与状态 / 财务标签多选 / 微信读书同步与统计 /
导航线上数据与图标

用户本轮 9 条需求(原话见 templates/ 与 app/ 内的逐条注释):

1. 名册:列表里给某人分组改为 AJAX 就地挪卡片(不再整页刷新)。
2. 纪念日:关联人改**多选**(复用 _person_picker,字段 person_ids[]);
   自建纪念日的按钮与其它模块统一(改=蓝 .btn、删=黄 .btn-warn);
   列表新增「关联人」列并给人名前加头像。
3. 首页:日程/任务两块**完全等高**(align-items:stretch + 固定总高 + flex 子项
   min-height:0);修「0 件待办」——数字与右侧列表同源(原来各算各的)。
4. 🔴 名册里被移出的人过一阵又回来:根因是 PersonRoster::seedFromPersons()
   **每次 Schema::ensure() 都无条件重灌**(线上实测每跑一次 +1 行)。
   修:给它加自守卫(只有在 persons 还留着 group_id/sort_order 的**老库**才跑)。
5. 名册:点击=看详情、**只有拖 ⠿ 手柄**才是排序(原来整行既能点又能拖,语义混乱)。
6. 任务:所有任务前加复选框,任何位置点勾即完成;
   状态语义照业界整理为 To Do / Doing / Done 并写出说明(用户要求参考别人的做法)。
7. 财务:类别标签改**多选非互斥**(再点一次取消该类别),上下两图 + 图例 + 表格
   全部按同一个集合联动;高亮条刻意不给「合计」(多选下各类金额不能相加)。
8. 微信读书:新增**懒同步**(访问时顺手做,6h TTL,只补空字段不覆盖用户填的)、
   封面图**拉下来存本站存储**(原来只存外链 → 看不到)、接官方 /readdata/detail 统计。
9. 导航:线上数据只搬了一半(5 板块 29 站 → 7 板块 67 站 + 67 图标)、
   展示页头像不显示 / 搜索按钮错位 / 点登录 404。三个真根因:
   · 相对地址在 /nav/ 下全 404 → 新增 Nav::abs();
   · 搜索容器丢了 id="search"(CSS 是 #search button.submit 绝对定位);
   · Nav::iconKey() 与 SimpleNav 的 icon_key() **算法不一致**(少一次 md5 归一化)
     → 搬来的图标文件名对不上;且 uploads/ 整目录 Require all denied。
   新增两个**免登录公开出口** nav_icon.php / nav_avatar.php。

顺带修掉一个「数据损坏级」的真 bug(做第 4 条线上反证时发现的,之前一直没人注意):
app/Region.php 三处用中文标点当 trim/ltrim 的 charlist,而 PHP 的 charlist 是
**按字节**匹配的 —— 「。」= E3 80 **82** → 字节 82 进了可剥集合 →
「层」= E5 B1 **82** 被切掉尾字节 → 非法 UTF-8 → backfillAddress 的 UPDATE 报
`1366 Incorrect string value` → **地址拆分升级从上线起一直在失败被跳过**(每请求报一次)。
改成字符级 preg_replace('/…/u');线上实测 ensure() 现在无报错、待拆行数真的在下降。

验证战绩:
- e2e 1289/0(新增 Region 字节级用例等)
- 真机探针:选人 19/0 · 首页/导航 24/0 · 导航模块 34/0 · b18 暂存+确定 10/0 ·
  第23批 52/0
- 桌面几何 428/0 · 移动端几何 680/0 · 交互 92/0 · 窄屏 24/0
- 老库升级 153/0(含 anniversary_people 迁移 + 「再跑 ensure() 名册行数不变」)
- 部署静态断言 180 OK / 0 FAIL · 反向验证 3/3 + Region 三变体各被两条断言同时抓住
- 线上 26 个文件与本地**逐字节一致**;展示页 200、图标 66/66 200、登录 200、
  越权/不存在均 404、签发短语后 ensure() 不再报错。
2026-10-07 11:35:31 +08:00
laoyang 09e30d27d4 v24 第 24 批:选人冲掉已有成员 / 首页改版 / 个人导航 / 微信读书 / 财务图
用户 6 条原话(要点):
  ①「选择名册里面人员的那个弹出框有个问题,就是会把页面里面已有的人员给冲掉。
     比方说电影里面的观影人或者是旅程里面的参与人…这是一个 bug 应该通篇检查。」
    并按「输入拼音 → 空格选中第一个 → 输入框清空回焦 → 再输下一个 → 回车=确定」重做交互。
  ② 首页简要信息改成:问候 + 姓名 + 日期 + 星期 + 农历 + 节气/节日/纪念日/生日,
     下面「今天几条日程 / 几件待办」,天气**彩色图标**放**最右侧**,温度风力,最下面地址。
  ③「家里的镜框这里呃全部不要了」→ 删模块卡片墙 + 未来七天;日程与任务**左右各半**、
     高度固定、内部滚动且**不显滚动条**;日程**掐头去尾** + 当前时刻**红线**;
     任务显示标题+状态+**所在看板名**;最下面往年的今天。
  ④「把个人导航项目完整搬过来,放在读书后面」;免登录展示页 `/nav/?<短语>`;
     右上角给**登录**入口;展示页头像用**用户自己的头像**。
  ⑤ 读书加笔记 + 导入微信读书笔记(用户明确「**用微信读书 api key**」)。
  ⑥ 财务页第二张多线图不随类别标签筛选。

用户拍板(AskUserQuestion):① 首页删「模块卡片墙 + 未来七天」两块;
② 微信读书用官方 API Key;③ 导航短语「自己起名 + 可一键换随机码」都做;
④ 6 条一次做完再上线。

════ 一、 🔴 第 1 条:复现出的真 bug(全站性)════
**根因**:选人清单是**打开弹框时才建 DOM**(懒建,为性能),而承载"已选"的
checkbox 就在那份清单里 → **没打开过弹框就提交**时表单里一个值都没有 →
后端把"表单里没有 attendees"理解成"用户要把人全删掉" → **静默清空**。
实测复现:库里同行人 `2,3,4` → 只改标题保存后变成 **NULL**。

修法:模板加**承运字段**(`.pp-carry` 隐藏 input,承载当前已选);
JS 建清单时**撤掉**它(否则同一个 name 会带两份值)。
⚠️ 裸 POST(完全不带 attendees)**本来就该清空** —— 那是显式地"不选任何人",
所以断言也必须照着"浏览器实际提交的样子"(带承运字段)来测。

**键盘流程**(用户第 1 条后半):空格 = 选**当前可见列表里第一个还没选的**
(⚠️ 必须跳过已选中的 —— 否则会把它**取消**,与"我要选人"正好相反,本轮真机跑出来的缺陷);
选中后清空输入框 + 光标回输入框;回车 = 确定。空格必须 `preventDefault`
(否则输入框里多一个空格字符)。

覆盖范围:4 个多选(`attendees[]` / `members[]` / `shares[]` / 修订 `members[]`)
+ 全部单选(归属人 / 负责人 / 纪念日人等)。

════ 二、 首页改版(第 2、3 条)════
自上而下只有三块:
  ① 信息面板:问候(早/中/晚 + 姓名)→ 日期 / 星期 / 农历 → 日子标记
     (法定节假日 / 节气 / 传统节日 / 纪念日 / 生日,各一色)
     → 今日数字(今天几条日程、几件待办)| **最右**:彩色天气图标 + 温度 + 风力 + 地址
  ② 日程 / 任务 **左右各半**(各 360px 固定高、内部滚动、**不显滚动条**三套写法)
     日程是**日视图形式的刻度**、**掐头去尾**(首末日程前后各 1h)、**当前时刻红线**
     任务是标题 + **状态** + **看板名** + 截止
  ③ 往年的今天

新增 `Home::todayMarks()`(把五类来源合成一句话:"今天是国庆节 · 大暑")与
天气的 `tone` 色调档(彩色图标靠 CSS 上色,**不用 emoji** —— 避免旧系统豆腐块)。
删掉:`mod-cards` 模块卡片墙、`未来七天`。

════ 三、 个人导航(第 4 条,SimpleNav 完整搬运)════
`app/Nav.php`(4 张 `nav_*` 表 + 稀疏排序 + 短语)· `app/NavFetch.php`
(抓取与图标生成,27KB,从原项目 functions.php 机械搬运,**原文那批"线上实测"注释全部保留**)
· `nav_show.php`(**全站唯一免登录页**)· `nav_suggest.php`(联想代理,凭短语门禁)
· `templates/nav/index.php`(管理页:板块/站点/引擎/设置 + 拖拽排序)。

搬运只改两处(不改会坏):① 独立 SQLite → 家书的 MySQL;② **所有查询加 `person_id`**
(原项目是单用户系统,不加这一维全家人会共用同一份导航)。

免登录展示页:`/nav/?<短语>`(也认 `/nav/?<短语>` 不带参数名);
短语用户自起名(4~32 位字母数字`_-`,全局唯一、保留词拦截)或一键换随机码;
右上角**登录**入口(进家书登录页);头像用**他自己**的头像;空态给一句话。

路由三处同步(`router.php` / `.htaccess` / 部署脚本 5c **独立标记块**)——
这是本仓库第三次复用这条规律:**每加一个"站根上的独立入口",就必须是自己的标记块**。
⚠️ 线上重写规则**顺序有语义**:`^nav/suggest\.php$` 必须在 `^nav/?$` 之前
(后者会把前者吞掉)。

════ 四、 微信读书(第 5 条)════
用户提示「用微信读书 api key」是对的 —— **我的知识过时了**:2026 年官方已开放
(网关 `POST https://i.weread.qq.com/api/agent/gateway`、`Bearer wrk-` 认证、
接口 `/user/notebooks`(游标 lastSort 分页)、`/book/bookmarklist`、
`/review/list/mine`、`/book/chapterinfo`)。

`app/WeRead.php`:Key 用 `Crypto::encrypt` **加密落库**、界面只回显掩码
(官方原话:这串 Key 等价于账号凭据);导入走「拉预览 → 勾选 → 导入」,
落 `book_notes`(去重 = kind + 内容 + 位置)。
⚠️ 业务参数必须**平铺在顶层**,不能包进 `params`(官方文档专门列了这个反例)。
另一条路「手动粘贴」保留(`books/notes_import`,早已有)。

════ 五、 财务第二张图(第 6 条,**上一批我引入的 bug**)════
`g.hidden = true` 对 **SVG 的 `<g>` 无效**(它没有 `hidden` 属性)→ 线条一条都没被藏;
**而我的探针也读同一个属性** → 两边一起绿(假绿)。
改为 `style.display`(恢复用 `''` 而不是 `'block'`);探针改读
`getComputedStyle(el).display !== 'none'`(落在**渲染结果**上)。
⚠️ "探针与实现读同一个属性"是**互相掩盖**的典型,比单纯写错断言更危险。

════ 六、 验证 ════
e2e **1218/0**(本批新增 102 条)· 真机探针 **19/0**(选人)+ **24/0**(首页/导航)
+ **34/0**(导航模块)· 桌面几何 0 失败 · 移动端几何 674/0 · 老库升级 149/0
· 部署静态断言空跑 **310/0** · **反向验证 13/13**(逐条改回旧行为,点名断言都变红)
· 线上验收:9 个新文件全在、4 张 nav 表建好(schema_version=24)、
  三条重写规则顺序正确、免登录页 / 空短语 / 错短语 / 联想门禁 全部响应正确。

════ 七、 本批的"夹具与断言"教训(都写进注释了)════
· `calendar_events.all_day` **默认值是 1** → 插夹具不写它就成了全天事件 →
  `time` 被抹成 `''` → "刻度掐头去尾"看着像没做(白查三轮)。
· 日历本要挑 `kind='custom'`(我原以为"普通本没有 kind",实际**普通本就是 custom**)。
· 量"同高/同排"要拿**内容区**(去 padding)比,不能拿 border-box(`.topline` 有 14px 上内边距)。
· 判据不能把**边界上的正确行为**当 bug(22 点有日程时末刻度**就是** 23:00)。
· 「HTML 写了、CSS 没写」e2e 永远查不出来 —— 导航展示页的登录按钮就落到了左边
  (实测按钮右 116px / 整行右 1210px),只有真机量几何才抓得到。
· 反向验证里 e2e 会**重铺副本**把注入覆盖掉 → 给 e2e 加了 `FB_PATCH_FILE` 钩子
  (重铺完自动打补丁),13 例里 8 例"没牙齿"其实是这个假象。

════ 八、 新增/改动文件 ════
新增:`app/Nav.php` · `app/NavFetch.php` · `app/WeRead.php` · `nav_show.php` ·
`nav_suggest.php` · `templates/nav/index.php` · `templates/books/weread.php` ·
`assets/css/nav-show.css` · `assets/js/nav-show.js`
改动:`app/Schema.php`(v24 + 4 张 nav 表)· `app/Module.php`(+nav)· `app/Home.php` ·
`index.php` · `templates/_person_picker.php` · `assets/js/person_picker.js` ·
`templates/home.php` · `templates/ledger/index.php` · `templates/books/_settings_menu.php` ·
`assets/css/app.css` · `router.php` · `.htaccess`
2026-10-07 05:52:40 +08:00
laoyang ae3060f552 v24 第 23 批:资源页四色按钮+四个弹框 / 财务页类别标签联动上下两图
用户 2 条原话:
  ①「资源页面,还可以更简洁,资源打开后,没有正文就不显示(没有正文)这句话了,
     直接显示附件。附件列完后,列出来人员,所有者、共享给谁了。然后其他的都不要。
     把编辑、共享、附件、删除,四个按钮,放在每个资源的右上角,和标题一排,
     点击后分别是编辑信息、设置共享人员,包括删除共享人等,附件,点击后弹框,
     允许用户拖动上传。编辑黄色、共享绿色、附件蓝色、删除红色。
     按钮点击后的功能,都是弹框。」
  ②「财务,统计功能和金额变化图表那里,中间,有各类标签,存款保险等。现在点击后
     没有任何效果,这个需要做处理,点击任何一个标签,则统计这个标签下的所有财务条目的
     总数,并在上下两个图上,画出来。」

用户拍板的两个岔路(AskUserQuestion):
  · 筛选某个类别时,上面那三个总数(总资产/总负债/净资产)**保持整本账口径**,
    不跟着切 —— 避免屏幕上只剩一个类别、却被误读成「我家只有存款」。
    当前看的是哪一类,由概览卡里一条高亮条说明(含合计与笔数 + 清除筛选)。
  · 「附件」弹框**既能拖动上传、也列出已有附件并可删**(附件管理只留一个入口);
    展开区的附件墙因此变成**只读**。

════ 一、 资源页(templates/resources/index.php 重写)════

**展开区只留「看」**(用户:"然后其他的都不要"):
  · 正文 —— **没有就不占位**(不再写「(没有正文)」那句话);
  · 附件墙 —— 只读(可预览/下载,不再带删除按钮);
  · 人员行 —— 列在附件之后:**所有者 + 共享给谁**。
原来堆在展开区底部的四坨内联表单(补传附件 / 共享设置 / 编辑 / 删除)**全部撤掉**。
⇒ 全页 <form> 的数量**不再随条目数增长**。这不是洁癖:账目页与名册页都因为
  「每行内联一套表单」被密码管理器(Bitwarden)逐个扫描、把主线程占死过两次,
  是用户报过的真问题。资源页这次一次做对。

**标题行右上角四色按钮**(用户点名配色):
  编辑 `btn-warn`(黄)· 共享 `btn-ok`(绿)· 附件 `btn-info`(蓝,本轮新增该类)·
  删除 `btn-del`(红)。四个都在 `<span class="res-ops">` 里,`margin-left:auto` 吸附右端。

**四个动作全在弹框里**(全页只有一份 `#resPanels`):
  · 编辑 —— 标题 / 正文 / 归属人(用选人组件的对外接口灌数据,且**限定在弹框内**查找
    ——本页有多个同名字段,只按名字找会命中第一个,财务页在这上面栽过);
  · 共享 —— 选人 + 权限(只读/可读写)+ **已共享给谁、逐个「取消共享」**
    (用户原话:「设置共享人员,**包括删除共享人**等」);
  · 附件 —— 已有附件(名字 / 大小 / 下载 / 删除)+ **拖动上传区**
    (复用全站 `_stage_drop.php`:点击或拖动、拖进来立刻上传);
  · 删除 —— 说明「不能撤销,会连 N 个附件一起删」,红色确认按钮。

三条结构性约束(断了浏览器会重新解析 HTML,症状很隐蔽):
  ① `<summary>` 里只能放 **phrasing content** → 按钮组必须是 `<span>`,`<form>` 绝对不能进
     (这也是「四个动作必须进弹框」的一条额外理由:它们本来全是 form);
  ② 点按钮必须 `preventDefault()` —— `<summary>` 内任意点击都会 toggle `<details>`;
     `stopPropagation()` 挡不住它(那是**默认行为**,不是冒泡);
  ③ 弹框容器自己声明了 `display:flex`,就必须补 `[hidden]{display:none}` 规则,
     否则「关不掉」(项目在 `.pv` 与 `.pp-item` 上各栽过一次)。

════ 二、 财务页(templates/ledger/index.php + Ledger.php + chart.php)════

🔴 **「点击后没有任何效果」的根因(本轮定位)**:
  图表卡那条类别 chip 的 JS 在找 `[data-ck-card]` 这个属性来显隐「每账目一张小卡」,
  而 v24 第 11 批 #7 按用户要求把「每账目一张小卡」换成了**一张合并的多线图** ——
  属性随之消失,**而这段 JS 没人删**。于是点标签:`.is-on` 会亮、画面上什么都不变。
  典型的「看着有交互(按钮会变蓝)、实际没作用」:**JS 找的钩子已经不存在了**。
  另外,概览卡里那些标签(存款/基金/保险…)原来是**纯展示的 `<span>`**,压根不可点 ——
  用户说的"中间,有各类标签"多半也指它。本轮把两处**并成同一套筛选入口**。

一处 `applyKind()` 同时做四件事(缺一件用户就会觉得"没生效"):
  ① 两处标签条的高亮**同步**(它们是同一个筛选器的两个入口,只给一处绑事件必然状态不一致);
  ② 上面那张「总金额」图切到**该类别的曲线**(按类别**预渲染**好,靠 hidden 切换
     —— 图是服务端画的 SVG,点了再请求就等于整页刷新,那就不是"点标签"是"点链接"了);
  ③ 下面那张多线图只留该类的线(`chart_multi` 已给每条线标了 `data-ck-line`)
     + 图例同步;
  ④ 下面账目表只列该类条目(`data-ck-row`)。
另外加一条概览高亮:「正在看『存款』:合计 ¥X · N 笔」+「清除筛选」。

服务端口径:
  · `Ledger::timeline($ledgerId, $kind = '')` —— **在取时点之前**按类别过滤。
    ⚠️ 反面做法(先算整本再筛)会把「被筛掉那些账目的创建日」留在时点集合里,
       图右端就凭空多出几个"其实什么都没变"的时点;
  · `Ledger::summary()` 顺手给出 `by_kind_n`(每类**笔数**)——
    用户要的「统计这个标签下的所有财务条目的总数」;
  · `chart_multi` 给每条线包一个 `<g data-ck-line="kind">` —— **纯标注、不改几何**。
    同一份数据多次渲染必须逐字节一致,所以属性值只能是 kind 本身,不许拼时间戳之类的。
  · 三个总数字**不随筛选变**(用户已确认)。

════ 三、 本轮修掉的三个真问题(都不在"功能"层面)════

① 🔴 **覆盖式改 CSS 漏了子属性 → 四个按钮竖排、标题行被撑到 175px 高**
   `.res-ops` 有一条**旧规则** `flex-direction: column`(那是给"展开区四个内联表单竖排"
   写的)。我改完模板后只在文件末尾**追加**了 `display / gap / margin-left`,
   **没写 `flex-direction`** → 四个按钮仍然竖着排。
   真机几何实测:`ops.top=382.9` vs `title.top=445.6`(差 63px)、标题行高 175px。
   ⇒ **改一个类的语义时,旧规则要就地改/删,不要在后面叠一条覆盖** ——
     叠加的写法天生容易漏子属性,而且**下一次改的人只会看到其中一处**。
     这次把那条规则就地改掉,并删掉确已废弃的 `.res-size` / `.res-share-add`。

② 🔴 **静态断言被注释里的字面串撑着 → 恒绿(比恒红危险)**
   部署断言原本是 `grep -qF 'class="res-ops"' 模板`。反证时我把**真代码**改坏,
   断言**照样通过** —— 因为模板注释里(解释"为什么按钮组用 span"时)引用了同一个字面串。
   **恒红会被发现,恒绿会一路伪装成"有覆盖"。**
   ⇒ 判据锚定到代码行(`grep -qE '^[[:space:]]+<span class="res-ops">'`),
     并且注释里不再写完整字面串。

③ **`str.replace(old, new, 0)` 是「替换零次」** —— 写反证脚本时我给某例传 `0`
   想表示"替换全部出现",实际什么都没改 → 报"断言没牙齿",白查一轮。全部替换是 `-1`。

════ 四、 验证 ════
- e2e **1171/0**(本批新增 55 条)· 真机浏览器探针 **45/0**(新增)
- 部署静态断言空跑 **309/0** · 桌面几何 428/0 · 移动端几何(360/375/390/414)684/0
- 老库结构升级 149/0
- **反向验证 18 例**(`_fb_b23_falsify.py`):按钮组拿掉 / 附件按钮摘掉蓝类名 /
  弹框容器拿掉 / 写回「(没有正文)」/ 两处上传区 id 撞名 / 取消共享表单删掉 /
  按钮组改用 div / 去掉 preventDefault / 删掉 [hidden] 规则 /
  写回 flex-direction:column / 「清除筛选」不响应 / 线条不标类别 /
  上图不预渲染 / 账目行不标类别 / timeline 去掉类别筛 / summary 不给笔数 /
  部署断言两条 —— 逐条改回旧行为,点名断言都变红,还原后复绿。

真机探针验的是**行为与几何**(curl 级断言看不见的东西):
  四按钮横排且贴标题行右端 · 点按钮**不会顺带展开**这一条 ·
  弹框 ✕/Esc/遮罩 三种方式都关得掉 · 点标签后**上面那张图真的换了**、
  下面多线图只剩该类 · 账目表跟着收窄 · 高亮条出现且「清除筛选」能回到全部 ·
  375px 窄屏两页都不横向溢出。

════ 五、 新增/改动文件 ════
  重写 `templates/resources/index.php`
  `templates/ledger/index.php`(筛选条合并 + 预渲染各类曲线 + applyKind)
  `app/Ledger.php`(timeline 加类别参数 + summary 补 by_kind_n)
  `app/chart.php`(chart_multi 给线条标 data-ck-line)
  `index.php`(ledger 路由多传 $kindLines)
  `assets/css/app.css`(.res-ops 就地改 + 四色按钮 + 弹框外壳 + .led-focus + 窄屏)
2026-10-06 20:41:04 +08:00
laoyang ecf121772f v24 第 12 批:5 条(删除按钮位置/日期控件三修/日历跳转/节气节日/财务图)
用户 2026-10-05 的 5 条。第 11 批交付后用户追问"确定这些需求都做完了?"—— 没有,
这 5 条是进一步要求。实现过程中查出 4 个真 bug。

#1 详情页删除按钮紧贴「开通账号」
   上一版放在页面最下方独立区块(danger-zone),用户要「就放在开通账号按钮下面,
   紧挨着,长度大小和开通账号一样,颜色改成红色」。→ 搬进「登录账号」卡片内
   (.del-inline + 细分隔线 + .btn-block-lg)。
   线上几何实测:同一张卡片、左边缘对齐 255==255、宽度一致 990==990、红色 rgb(192,83,63)。

#2 日期控件三修(两个真 bug + 一次闪烁)
   · 灰底:Semi **外层** .semi-datepicker-input 自带 rgba(46,50,56,.05)、高 32px;
     内层 wrapper 白底 40px → 灰的露在外面。实测「灰底元素」= 1 个 → 修后 0 个。
   · 不垂直居中:外层 32px < 内层 40px,实测 gapTop=1 / **gapBottom=17**(文字贴顶)
     → 修后 gapTop=1 / gapBottom=1。
   · 闪烁:实测首 **200ms** 原生输入框可见(要等 3.4MB React 解析)。
     用 no-js/js 二态:<html class="no-js"> + head 内联脚本立刻换 js +
     CSS `html.js .dt-input:not(.is-semi) .dt-native { visibility:hidden }`
     (配同高骨架 ::after 防布局跳动)。实测「出现过原生可见」= False。

#3 日历跳转保留视图 + 下拉日历
   原来写死 view=day(在月视图点跳转会被踢进日视图)。→ 表单带当前 $view(月还带 ym);
   选择器从"居中弹窗"改成**按钮下方下拉** .cal-jump-drop。
   实测:月视图跳 8/15 仍在月视图且是 8 月;周视图跳 8/15 → 落 8/10 那一周;
   日视图跳 8/15 → 显示 8/15。

#4 传统节日 + 二十四节气两个系统本(Schema v18)
   Holiday::SOLAR_TERMS(24 项 C 常数近似公式 + 例外表)+ traditionalRange
   (农历推导;除夕用"次日是否正月初一"反推,写死腊月廿九会有一半年份错)。
   实测 2026:节气 24 条全对(清明 4/5、冬至 12/22);春节 2/17、除夕 2/16、中秋 9/25。
   ⚠️ ENUM 必须显式 ALTER(对已存在的表不生效)——同一坑第二次踩,VERSION 17→18。

#5 财务(一个真 bug + 一个假起点 + 一个真 bug)
   · chart_line 读 `cents` 而 timeline() 返回 `net` → 键名不匹配,**总资产图每点读成 0**
     (正是用户说的"图上是 0")。修:兼容 cents ?? net ?? asset。
     实测 timeline net=800000(8000 元),线上 polyline 有真实起伏。
   · 去掉 timeline 人为插的"前一天=0"假起点(用户:"总资产不应该是 0,
     应该是所有账目创建时的金额的总数")。
   · ppSetSelected 只用 document.querySelector 按名字找,而财务页有**两个** person_id
     组件(「记一笔」+ 修改弹框)→ 永远灌到第一个。加 root 参数、调用方传 panel。

验证:e2e 786/0、交互 92/0、升级 129/0(+3 条 v18 断言)、batch_e 91/0、batch10 46/0、
batch9 38/0、picker_unify 28/0、picker_lazy 11/0、crop 17/0、formbloat 9/0、semi 27/0、
mobile 688/0、drag 5/0、geom 5/5;第 12 批专项:财务 7/0、日历 19/0。
部署静态断言新增 5 组(含反向验证:注入回退 → FAIL=1)。
线上自检 19/19;线上真实账号验收 **30/0**(含真实浏览器几何量删除按钮位置)。
2026-10-04 19:43:00 +08:00
laoyang 5d80b9480b v24 第 11 批:7 大条 20 子项(首页/名册/观影/裁剪/日程/财务)
用户 2026-10-04 第 11 批,7 大条:
- #1 首页:未来七天过滤掉「农历」(每日一条会淹掉真日程);右侧日历本标签
  从灰字(.dash-tag)改成**实心色块**(底色=日历色、白字、text-shadow 兜底浅色)。
- #2 名册:分组改**居中模态框**(用户要"像选观影人一样")。修掉"点第二次失效"
  ——根因是旧实现用**捕获阶段**监听 document click 做"点外面就关",与触发按钮
  的 click 打架;模态把关闭收敛到遮罩/✕/取消。卡片「编辑」后加「删除」
  (→ people/destroy);详情页底部删除改成 btn-danger + btn-block-lg 大红按钮 + 两步确认。
- #3 名册详情页重排:.detail(260px+1fr) → .pcard(头像左上 + 右侧姓名/标签)
  + .detail-stack 全宽往下。旧布局头像卡比右栏矮,头像下留一大片空白。
- #4 观影:影院下拉换**自定义组件**(原生 <option> 画不了色块)——按钮+列表、
  每行色点,值仍写隐藏 input[name=cinema_id];「管理影院」→「新建影院」弹框
  (带 back=movies/new,存完回本页并 ?cinema=<id> 自动选中)。
  修「开始/结束时间标签跑到控件下面」:upgradeRange 原来 insertBefore(host, group.firstChild)
  把控件插到了 label 之上 —— 改成插在第一个原生控件之前。
- #5 裁剪改 **Cropper.js**(assets/vendor/cropper/,按需加载走 fb-cropper meta):
  弹框 overflow:hidden + 固定画布(**无滚动条**)、**去掉缩放条**改滚轮 wheelZoomRatio、
  viewMode:1 + autoCropArea 保证框小于画布、8 个触点全露。
  结果仍写 *_crop = 归一化 x,y,w,h(提交契约不变)。
- #6 日程:内置节假日原来每天只标**一天**(国庆只出一条)→ FIXED 改成
  [名字, 放假天数] 按区间铺;月视图从周日改**周一**起(表头同步);
  跳转日期从"长条+原生框"改成**单个 btn-icon**,点开弹 Semi 日期选择器。
- #7 财务:单条曲线无盘点时只回一个点(图看着像全 0)→ 改成"从创建日(0)画到今天";
  图下条目从"每账目一张小图卡"改成**一张 chart_multi 多线图 + 颜色图例**
  (Ledger::LEGEND_COLORS 按 id 取模,保证颜色稳定);账目页卡顿同源治理
  (行上只留按钮、表单全页一份 #ledPanels);修改按钮改弹框并回填归属人。

## 顺带修的真 bug(本批最要紧的三条)
1. **观影表单提交静默失效**:「新建影院」弹框被误嵌在观影主表单 <form> 内。
   HTML 不允许 form 嵌套 → 浏览器拆开内层,弹框里 required 的"影院名称"变成
   主表单必填项、又因 hidden 无法聚焦 → 点「记下这场电影」什么都不发生
   (Chrome: invalid form control with name='name' is not focusable)。
   → 弹框移到主表单之后。部署脚本加防线:按顺序配平 form 开闭判断是否嵌套。
2. **裁剪取消没清空文件框**(换 Cropper 时丢)→ close(cancelled),4 个取消入口
   传 true 清空;否则"点了取消"仍会把没裁的原图传上去。
3. **裁剪预览退化成原图**(同批丢)→ 用 canvas 按 Cropper 的原图像素矩形重画,
   toDataURL 写回预览,恢复"所见即所得"。

## 连带修复:选人组件懒渲染(线上实测的必要性)
线上真实数据下账目页 6939 KB / 16788 个 input、观影表单 1194 个 input ——
根因是 _person_picker.php 首屏就把**全部候选人**渲成 <label>+<input>。
改成:首屏只留「不选」+ data-pp-people(JSON),**打开弹框时**才 buildList();
无 JS 兜底走 <noscript>(HTML5 规定启用脚本时其内容按纯文本解析,不产生节点)。
新增 window.ppSetSelected / decorateItems(动态项要补算首字母)。
实测账目页 6939 KB → 544 KB。新增 _fb_picker_lazy_check.py(11/0 + 反向 2 红)。

验证:e2e 786/0、交互 90/0、半集成 27/0、batch_e 91/0、batch10 46/0、batch9 38/0、
crop 17/0(+反向)、picker_unify 28/0、picker_lazy 11/0(+反向)、formbloat 9/0、
upgrade 126/0、mobile 688/0、drag 5/0、geom 5/5。
线上自检 19/19;线上真实账号验收 7/0(含"主表单无隐藏必填""线上 Cropper 接管+取消清空")。
2026-10-04 17:42:21 +08:00
laoyang b2ca4f336e v24 第 10 批:名册真性能(根因是每卡内联表单)等九条
- #6 名册线上真实数据(1184 人)卡顿:根因是**每张卡片内联一整份分组弹层**——
  全页 2369 个 form / 27234 个 input / 22496 个 radio / 98100 DOM 节点,
  密码管理器(Bitwarden 等)逐个扫描 form 把主线程占死(用户报的就是这个冲突)。
  改为卡片只带 data 属性 + 全页**唯一一份**共享浮层 #rosterMenus。
  实测:form 2369→3、input 27234→25、DOM 98100→17659、解压 7694→2306 KB、
  长任务 1/52ms → 0。新增哨兵 _fb_formbloat_check.py(含反向验证)。
- #5 日视图翻页失效:day_columns() 无条件吸到周一,天视图只有 1 列 → 永远显示那周周一。
  改为仅 span>1 时吸附(周视图行为不变)。
- #2 财务「欠款」类别(负债);并修一个真 bug:负债填负数会让净资产**反向变大**。
  写入口 normalizeAmount 归一化 + summary/timeline 侧 abs 防御 + Schema v17 纠正老数据。
- #3 账目行「盘点」「修改」两个并列按钮(各自就地弹面板);图移到账目上方,
  总金额图跨整行 + 每个账目一条线 + 类别筛选条;charts() 改为覆盖全部账目。
- #4 日程新增「农历」「节假日」两本不可删除只读日历(app/Holiday.php):
  两级自动更新 —— 内置规则(公历固定 + 清明节气 + 农历节日)保证断网也有;
  官方 JSON 镜像自动抓取补齐调休。抓取**不在渲染路径联网**(响应后 fastcgi_finish_request
  + 频率闸门),缓存落 storage/cache。仅派生本不可写日程。
- #6 日程页设置按钮前加「跳到某一天」(选中即跳)。
- #7 起止范围改用一个 Semi dateTimeRange 控件(观影/旅程/节点/日程共 4 处);
  新增 window.fbSetDT / fbUpgradeRange;修范围重挂(React 非受控组件须 unmount 再 createRoot)
  与范围里 required 未摘除导致表单无法提交两个真实问题。

验证:e2e 786/0、batch10 44/0、batch9 38/0、batch_e 91/0、upgrade 126/0、picker 28/0、
semi_integrate 27/0、formbloat 5/0(+反向 3 红)、mobile 688/0、interact 97/0、drag 5/0、geom 5/5;
线上自检 19/19 + 线上真实账号巡检 28/0(含 Firefox 实测)。
2026-10-04 10:46:50 +08:00
laoyang e0562b8627 v24 第 5 轮:十条清单(天气填城市 / 输入即筛选 / 看板列=看板 / 账本盘点折线图 / 纪念日分类)
用户 2026-10-03 的十条,逐条落点:

1. 天气改为**个人设置里填城市**(每人一份):新增 app/Geo.php,内置 197 个
   地级市坐标(离线、逐条核对,生成脚本 _tooling/_fb_geo_gen.py)。填坐标仍
   作为内置表没有时的出口;老的家庭级坐标保留为兜底,升级当天天气不会凭空消失。
2. 所有"找个人"的地方**输入即筛选**:新增 assets/js/filter.js(纯前端显隐,
   不发请求、回车不刷新)+ app/PersonQuery.php 把服务端判据收敛成一份
   (名册 / 挑人 / 高级搜索共用,避免三处实现漂移)。
3. 名册列表:有昵称就不重复显示本名、行变矮、补齐手机/阴历阳历生日(带年份)/
   邮箱/工作单位;「清除」与「搜索」同款同尺寸。
4/5. 日程:撤掉左侧日历本栏改满宽,功能(显隐/颜色/导入/默认视图)全收进
   右侧 ☰;**色块高度改为按起止时长**(旧实现一律 30 分钟,三小时的会跟
   十五分钟的同高);点日历空白处即建日程,开始时间=点的那一格。
6/10. 资源:搜索提到最上;按"最近改动/最新新增"排序,默认只显示最新 10 条并
   给「显示全部」出口。
7. 任务:看板的**列=用户自定义的看板**(不再是写死的待办/进行中),
   拖动/左右键=换看板(改 board_id,不动 status)。
8. 财务:概览在上账目在下、记一笔改右侧按钮、账本设置(共享/改名/新建/删除)
   收进 ☰、默认账本名「xxx的账本」、"挂在谁名下"默认自己且用标准选人框;
   **新增盘点**(新增 ledger_entry_snapshots 只增不改的历史表,旧值不被清空)
   + 上面画金额变化折线图(app/chart.php 服务端画 SVG,零依赖)。
9. 纪念日:手动添加的与名册生日派生的**分两块**、手动的在上;右侧设置可增删
   分类(家庭级,删分类不强改已用的历史)。

本轮另外修掉/加固的四类问题(都由新增的静态或运行时守卫兜住):
- index.php 少写 2 个 use 导入(Pinyin / UserSetting)→ 名册与个人设置
  返回 200 但正文是 Fatal;顺带修了一处早已存在的 Attachment 未导入。
  → 新增 _fb_import_check.py(1f 段)。
- 一次不精确替换弄丢了 app/Calendar.php 开头的 `<?php`,整份源码被当 HTML
  吐给浏览器,而 `php -l` 报"无语法错误"(纯文本输出确实合法)。
  → 新增 _fb_php_open_check.py(1e 段)。
- 模板片段 unset 了宿主模板的变量(账本片段 unset($canWrite))→ 正文里一片
  Undefined variable 而 HTTP 仍 200。同 _datetime.php 早年 $t 的坑。
  → 新增 _fb_fragment_scope_check.py(1g 段)+ _fb_batch_e.py 的 26 页无告警巡检。
- 窄屏下页头动作区不换行 → 周视图横向溢出且按钮被挤成三种高度(用户提过三次
  "按钮要同高")→ .page-head .actions 加 flex-wrap。

验证战绩(全绿):
  e2e 773/0 · 第 5 轮专项 91/0 · 结构升级 126/0 · 几何 428/0 · 移动端 684/0 ·
  交互 97/0 · 家谱 26/0 · 按钮 60/0;三轮反向验证各 3/3 抓到,还原后复绿。
线上:schema v14、指纹一致(9df6c9fccc6796db)、snapshots 表已建、197 城、
  8 个关键文件 md5 与本地逐字节一致、app/ 403 无源码泄漏、错误日志无新增。
2026-10-03 15:01:38 +08:00
laoyang 8f80f3e835 v24 第 2 轮:19 条清单收尾(附件预览/CalDAV/任务分类看板/全局选人/家谱/纪念日/财务)
用户 2026-10-02 提的 19 条需求,本轮完成剩下的 8 条 + CalDAV 服务端:

- ③ 附件:图片/视频给缩略图,PDF/Word/Excel 给**同尺寸**图标;可预览的在本页
     弹层里看(不再新开标签页),渲染不了的点了直接下载;预览层右上角恒有下载按钮。
- ⑦b CalDAV:把「我的日历本」按 RFC 4791 只读子集分享出去(OPTIONS/PROPFIND/
     REPORT/GET,写方法一律 403)。用**专用口令**(库里只存 sha256),不是登录口令。
- ⑨⑩ 任务:分类与看板都由用户自建;已完成的任务从看板消失,改在「已完成」页
     按分类查看/搜索;设置入口在「新建任务」旁的 ☰。
- ⑪ 全局人员选择器:一个片段铺到观影/旅程/资源/日历共享/任务共享/财务共享;
     修掉了老组件「名字和拼音都搜不动」的 bug —— .att-item 自己声明了 display:flex,
     压过了浏览器 [hidden]{display:none},过滤根本没生效。
- ⑫ 家谱:头像改方形不再裁切;姻亲不在图上的卡片给「⇄ 姻亲」入口,
     以 TA 为中心重画 + 「返回我的家谱」。
- ⑬ 纪念日:阴历/阳历都能设且按对应历法重复;名册里每个人的生日默认可见
     (实时派生,不复制一份);全部作为全天日程进日历。
- ⑮ 财务:账本可共享,双方都能记账、都能看全部账目,资产−负债=净资产;
     金额一律以「分」存,不用浮点。

验证:端到端 724 项通过 / 0 失败;几何 368、交互 97、移动端 516、家谱 24、
升级 123 全绿;SQL 列名静态校验 0 处可疑;部署 11 条线上探针全过。
2026-10-03 08:18:05 +08:00