42aff02687fc3d6a1dda1d2c7bfe011703e6dd42
88
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4941e9db9c |
fix(home): 从浮层把任务标成完成时该行不消失(只修了复选框那条路)(#20)
用户第二次提同一条需求。核了线上代码:4 文件 md5 与本地一致、mtime 18:14, 部署没问题;复跑原判据仍 20/0 全绿。判据绿 + 用户能复现 ⇒ 判据有盲区。 盲区:首页把任务标成完成有**两个入口** ① 每行前面那颗复选框 —— home.php 末尾的脚本(上一版已修) ② 点整行弹浮层 → 面板里「状态」三段按钮 → assets/js/task_panel.js 的 syncRow() 上一版只改了 ①。syncRow() 只改徽章与复选框的 aria 状态,**从不删行**, 所以从面板点「已完成」后行留在原地,只把徽标换成「已完成」。 用户点的就是 ②,而原判据 grep 全文没有 panel / 浮层 / data-fb-entry —— 完全没测这条路。 改法: - 把删行逻辑抽成 window.fbHomeTaskDrop(row, wasStatus) 放 assets/js/app.js, 两个入口共用一份。作用域限定 [data-tk-home-host]:任务**看板**页的行也带 data-task-row,那里勾完成只改状态、**必须保留该行**(看板按状态分栏,删了就没处看)。 - task_panel.js 的 syncRow() 在 d.status === 'done' 时调它; wasStatus 必须在改写 data-tk-status **之前**读,否则概述卡不知道该减「在做」还是「待办」。 - home.php 原来那份本地实现改成薄转发,避免两份实现改一处忘一处。 - 顺带修掉一条说谎的注释:原注释写"补偿了一个可撤回的提示",但代码里从来没有 撤回按钮,只有一句 3.2 秒自动消失的 say()。 - _card_tasks.php 补记"两个入口"这件事。 子任务安全:task_panel_json() 用 panel_of 找**父任务**,回的 status 是父任务的, 所以面板里勾子任务不会把父任务那一行删掉(判据 ⑱ 专门守这条)。 判据: - 新增 _b38_panel_done_drop.py(21 条,走真实浮层点击): 修复前 **16 通过 / 5 失败**(⑥⑦⑧⑬⑭)→ 修复后 **21 / 0** - 注入反证 _b38_falsify.py:把删行那两行用 if(false) 挡掉,锚点断言 count==1, 立刻红回同样的 5 条、关键条目 ⑥⑧⑬⑭ 命中 4/4,副本自动还原 - 原 _b37_home_task_drop.py(复选框路径)仍 **20 / 0** - _b37_regress.py **11 / 0**:看板仍改状态且**不删行**、详情页子任务复选框正常、 空态 display 真的是 none - home.php 内联 JS 0 块坏;app.js / task_panel.js node --check 全过 |
||
|
|
ea6e42c27f |
fix(home): 近期任务勾选完成后该行不消失(连概述卡计数也不减)(#20)
用户原话:「首页,近期任务,如果勾选了完成,应该刷新一下这个列表,把完成的删掉。」 ## 原来的行为 首页那段复选框脚本 AJAX 成功后只改徽章文字(`kb-st` → 「已完成」), **没有任何"移除这一行"的逻辑**。而且 templates/home.php:160 那条注释还写着 这是**当年的刻意设计**: > ⚠️ 改完**不摘掉这一行**:用户点完正想看自己刚完成什么,立刻消失会让人怀疑 > "是不是点到删除了"。刷新时由服务端决定它还在不在。 用户现在明确推翻这条:完成的就该从列表里删掉。 ## 服务端本身是对的 首页路由只取 `doing` + `todo`(index.php:839),`done` 本来就不在列表里 ⇒ 手动刷新这一行确实会消失,问题纯粹在"AJAX 完成后前端没同步"。 所以这次一行服务端代码都不用改。 ## 改法 新增 `dropTaskRow(row, wasStatus)`,AJAX 成功后(**按服务端返回的 `d.status` 判断**, 不是拿点击前的 `next`)做三件事: 1. **删掉这一行**; 2. **同步「概述」卡右下的「N 件在做 / N 件待办」** —— 那两个数字与这份列表 **同源**(同一个 `$tasks`,见 _card_brief.php:53),只删行不减去数字 ⇒ 列表 2 条、概述说 3 件在做,自相矛盾; 3. **一条不剩时把空态提示露出来**(服务端渲染的 `[data-tk-empty]` 原来一直 hidden) —— 否则卡片变成一片空白,用户以为加载坏了。 配套的小改动: - `_card_tasks.php`:空态提示改成**一直渲染、靠 `hidden` 切换**,加 `data-tk-empty` 钩子(不靠匹配文案 —— 文案改一个字就失效)。外层加 `data-tk-home-host`。 - `_card_brief.php`:两个统计 `<a>` 加 `data-hbs="doing|todo"` 钩子, 同样**不靠中文文案定位**(这是 JS 要读的东西,不能把展示文案变成接口)。 - `app.css`:补 `.empty-hint[hidden] { display: none !important; }` —— 浏览器默认的 `[hidden]{display:none}` 优先级最低,哪天有人给它加 `display:flex` 这句提示就会在"有任务时"也冒出来(与 app.css:601 那条注释是同一约定)。 ## 几个判据上的讲究 - **按 `d.status` 判断而不是 `next`**:`tasks/status` 是幂等接口, 拿服务端返回的状态才不会"手快连点两下,第二下把行删了"。 - **`wasStatus` 要在改 `data-tk-status` 之前存**:删行时要靠它决定减「在做」还是「待办」。 第一版直接在删行时读 `data-tk-status`,读到的已经是 `done` —— 自己写的时候就踩了。 - **提示文案补一句"已从近期任务移出,去「全部」里能看到"**: 删行的原罪是"用户以为点到删除了",所以必须**明说它去哪了**, 而不是沿用服务端那句「已标成「已完成」」(那会让人以为行还在,下一眼发现没了更慌)。 ## 验证 **主判据 `_tooling/_b37_home_task_drop.py` —— 20 通过 / 0 失败** 夹具:把库里历史 doing/todo 任务临时改状态腾空卡片(**不能靠 `deleted_at`** —— `Task::byStatus()` 的 SQL 里根本没有 `deleted_at IS NULL`,软删对首页无效,实测过), 造 2 条 doing + 1 条 todo,跑完逐个恢复。 覆盖:勾「进行中」→ 行移除 + 「件在做」2→1 + 「件待办」不误减; 勾「待办」→ 行移除 + 「件待办」1→0 且链接藏起; 删到最后一条 → 0 行 + 空态露出;刷新后与服务端事实一致;库里状态真的是 done。 **注入反证 `_tooling/_b37_falsify.py` —— 三次独立注入,各自红在对应处** | 注入 | 结果 | 命中的判据 | |---|---|---| | ① 不删行(原始 bug) | 通过 12 / **失败 8** | ③④⑤⑪⑭⑮ | | ② 删行但不减概述卡计数 | 通过 17 / **失败 3** | ⑤⑫⑯ | | ③ 不限空态(删完一片空白) | 通过 19 / **失败 1** | ⑮ | 每次注入前断言锚点唯一(#474),恢复后回到 20/0。 **回归 `_tooling/_b37_regress.py` —— 11 通过 / 0 失败** 看板勾选仍能改状态且**不删行**(与首页行为不同是有意的)、 详情页子任务复选框仍能改状态、`[hidden]` 的 `display` 真的是 `none`、 `data-hbs` 钩子在、卡片「全部」入口仍指向 tasks。 ⚠️ 回归里踩了一个判据假设错:我一开始按"详情页也该有父任务复选框"写, 直接 FAIL。真实情况是详情页的复选框**只给子任务**(show.php:181 在子任务循环里), 父任务没有 —— 是判据错了不是产品坏了(#429 同族)。 **内联 JS 校验 `_tooling/_b37_inline_js_check.py`**:把渲染后 HTML 的 `<script>` 抽出来逐个 `node --check`(`php -l` 不看 HTML 里的 JS)。 并做了反证:喂一段坏 JS,它确实报 1 块坏,不是恒绿。 工单 #20 |
||
|
|
ff818a3deb |
feat(nav): 导航站点支持自定义图标上传(含两处连带修复) #16
需求:网页版导航,新增/编辑导航地址时没有自定义图标功能。
## 功能
- 站点表单加 enctype=multipart/form-data,新增「自定义图标」控件:
40px 预览 + 选择图片 + 清除自定义图标(仅编辑且有自定义图时出现)
- 选择文件后本地即时预览(URL.createObjectURL,用完 revoke,不落 base64)
- 按文件内容判类型(finfo + <svg 探测),不信任扩展名;上限 1MB
- icon_custom 三态语义:不选文件=不动 icon 列;选文件=icon_custom=1;
勾清除=icon='' & icon_custom=0。普通改名不碰 icon 列,不会丢图
- saveUploaded() 改随机文件名。原实现按 md5(icon_key(url)) 命名,
单用户系统没事、多用户会互相覆盖
- 新增 Nav::iconFileInUse():图标文件按内容共享,删旧文件前先查还有没有
别的记录在用
- 图标从 uploads/ 目录取不到(线上 403),走 /nav_icon.php?f= 公开出口
## 连带修复 1:ICON_STORE 常量从未定义
SimpleNav 搬迁时漏搬 define('ICON_STORE', 128),NavFetch.php 第 667/739
行在用。PHP 8 抛 Error: Undefined constant "ICON_STORE",只要走到 GD
位图分支就 500。因为 saveUploaded() 一直是死代码(有定义无调用点),
这个错一直没被触发过。现补进 app/bootstrap.php(带 if (!defined) 保护)。
## 连带修复 2:22 处裸函数调用缺 self::
搬迁时把自由函数改成了 NavFetch 的静态方法,但类内部调用点没跟着改。
PHP 报 Error: Call to undefined function App\http_get() @ NavFetch.php:171。
这既是「全部更新」按钮 500 的根因,也是「图标一直抓不到」的真相 ——
整条抓取链路(http_get / attr_value / absolutize / third_party_icon_urls /
default_icon_candidates / meta_content / parse_icon_links / manifest_icons /
is_third_party_icon / looks_generated_tile / grab_icon / ensure_utf8)
一处都没跑起来过。
修后 CLI 实测 NavFetch::meta('https://www.baidu.com') 返回
name=[百度一下,你就知道]、icons=3、图标落盘 3818 字节。
## 验证
- php -l 5 个文件全过
- 端到端 21/0:真实上传 PNG → 列表出 img.nav-ico → nav_icon.php 返 200
+ PNG 头 → 编辑换图 → 清除 → 只改简称不丢图标 → 清理
- 语义验收:带自定义图的站点点「全部更新」后 icon 文件名不变、custom=1;
不传图的站点 custom=0(以服务端回报条数为证据,不看"未变")
|
||
|
|
eaa74bc593 |
fix(nav): 「我的短语」输入框被撑成 200px 高 #17
用户原话:「地址设置,短语那里,输入框怎么那么高啊。」
病根:模板把 input 包在 .field 里,而 .field 本身又是 .inline-form 的 flex 子项。
.field 是 flex-direction: column ⇒ 主轴是垂直方向,
于是 `.inline-form .input { flex: 1 1 200px }` 的 flex-basis
落在了**高度**上,再叠加 flex-grow 继续拉长。
.inline-form 的 align-items:center 又让它垂直居中,
看起来就是"一个很高的输入框 + 旁边孤零零一个按钮"。
线上实测(部署前,Playwright 量真实 rect):
短语输入框 h = 200.00px ← 正好等于 flex-basis
同页正常 input h = 45.80px
差 154.20px
修法:换用专属类 .nav-phrase-form,不动通用规则。
· 外层横向 flex(输入区 + 按钮),只把输入区当 flex 子项;
· 输入区内部仍走 .field 的纵向堆叠,并显式把内部 .input 的 flex 收回去
(这一条是关键:不收的话 flex-basis 又跑回垂直轴);
· 按钮 align-self:flex-end 跟输入框底对齐(tip 在下方,居中会显得按钮浮起来);
· 窄屏 ≤480px 按钮换行靠左。
为什么不去改 .inline-form 那条通用规则:
全站另有 15 处在用 .inline-form,且都是"input 直接当水平子项"的正确用法,
改通用规则会波及那些页面。本处是**孤例**(唯一把 .field 嵌进去的)。
验证(线上真实页面):
· 短语输入框 200.00px → 45.80px,与同页正常 input 差 0.0px
· label/input/tip 仍垂直堆叠;按钮与 input 垂直交叠 7.7px
· 回归:个人设置/任务看板/任务详情/笔记/名册/日程/微信读书/导航,
11 个 .inline-form 内的 input 全部 45.80px,零偏离
|
||
|
|
34625cd7d7 |
fix(web): 上传框点选后无反馈 + 同一文件重复入库(#15)
用户报:资源模块新建时点「上传附件」选文件,页面没有任何变化, 虽然文件实际传上去了、保存也成功,但界面看不出传没传成功。 根因:暂存上传框有三个入口,只接了两个 —— 点框、拖放都能用, **点选文件触发的 input change 事件没人监听**。 - 新增 bindStagePick():把点选接进 queueDrop(),与拖放走同一条链路, 点选后同样出缩略图卡片 / 状态文字 / 可撤掉(行为一致,不各写一套 UI)。 带去重指纹(name+size+lastModified),避免清空再选时传两遍。 - 修 slot 读错元素:data-stage-slot 渲染在框(.upbox)上而非 input 上, 原来 input.getAttribute() 恒为空串,槽位从来没传上去过。 对资源附件看不出差别,但海报/票根(2:3、按比例重裁)会拿到错槽位。 - queueDrop 增加「没清单就别跑」守卫并返回布尔说明是否接管。 顺带修掉一个我自己在第一版引入的 bug(流水线打回): 文件传完没有从 input.files 摘掉 ⇒ 同一文件同时存在于暂存令牌与 $_FILES 两条通路 ⇒ 服务端两条路径各入库一次 ⇒ 一张图存两行。 实测:点选 1 个文件 → attachments 2 行;观影第 35 批 2 张→4 行、编辑+1→6 行。 - 新增 dropFromInput():传完就把该文件从 input.files 摘掉(只摘一张, 多选时后面的还排在上传链里),只留暂存令牌一条路径。 验证: - 点选反馈 e2e 17/0(修复前 3 红) - 查库 e2e 8/0:点选 1 个文件 → attachments 正好 1 行(修复前 2 行) - 反向验证 10 条全过:三处注入各自精确变红,且不得波及拖放; 并固化「同样注入下渲染探针仍全绿」这一事实 - 观影第 35 批 13/3 → 16/0 - 全量流水线 2h37m EXIT=0 已部署,线上 md5 与本地一致 |
||
|
|
e826540d2a |
修复 PDF 预览被 Chromium 屏蔽(iframe sandbox 禁用内置阅读器) #14
现象:资源模块点开 PDF 附件,预览区只剩「此页面已被 Chromium 屏蔽」。
根因:preview.js 的 pdf 分支给 iframe 挂了 sandbox
(allow-same-origin allow-scripts,引入于 v24 8f80f3e)。
Chromium 只要看到 iframe 上有 sandbox 属性就禁用内置 PDF 阅读器插件,
表现即「被屏蔽」。五组同源对照实测(Chromium 153):
· 无 sandbox → 正常渲染
· allow-same-origin allow-scripts → 屏蔽
· allow-scripts → 屏蔽
· + allow-popups-to-escape-sandbox→ 仍然屏蔽(网上答案说能救,本版本不行)
· + allow-forms allow-popups → 仍然屏蔽
⇒ 加白名单无效,只能不加。
改动:删掉该 setAttribute,换成 17 行实测结论注释防止以后被"为安全"加回去。
安全性说明:PDF 由浏览器内置阅读器扩展在独立扩展进程渲染,不是普通页面
上下文,拿不到父页 window/DOM/cookie;PDF 是原生文档,没有能访问父页的
脚本环境。服务端另有 nosniff + MIME 内容嗅探(media.php),PDF 不会被当
HTML 执行。若将来要禁止 PDF 内嵌 JS,正确做法是换 pdf.js 自渲染。
验证:
· 新增 _tooling/_pdf_e2e.py,11 条断言全通过 0 失败,已登记进流水线
(挂「全路由」标签)。判据落在渲染层:frame 列表无 chrome-error、
有内置阅读器扩展 frame、iframe 内文字不含「屏蔽」。
· 反向验证:注入 sandbox 回去 → 4 条转红;恢复后 11/0。红绿闭环成立。
· 全量流水线 2h21m 跑完,EXIT=0,已部署。
· 线上 md5 与本地一致(988b804f),setAttribute('sandbox' 已不存在。
|
||
|
|
daf5ca904e |
v94 必填项改成「标签红色加粗」,去掉红色星号 (#3)
需求(用户 2026-10-25):必填项原来在标题后面加一个红色星号,排版经常不齐。
改成「标题本身红色 + 加粗」——用户习惯后,看到红色加粗的标题就知道是必填。
CSS(assets/css/app.css):
- :root 新增 --req:#B03A2E、--req-w:700。
· 不用 --red(#C0533F):那是动作红(删除按钮底色),当文字色放在暖纸底
#F7F3EA 上对比度只有 ≈3.4:1,13-14px 标签偏浅;#B03A2E 约 4.6:1。
· --req-w 必须 700 不能 650/600:本站标签基线字重本来就是 600
(.form-grid > .field > label / .card .field > label / .mv-row > .mv-lb /
.dt-range-label 四处都是 600),650 会被取整回 600 = 完全没加粗。
- 文件末尾新增「必填项统一标记」段,覆盖四种标签形态:
label.is-req / .mv-lb.is-req / .dt-inline-lab.is-req / .dt-range-label.is-req,
用 !important 打穿各宿主选择器的特异性(颜色与字重是语义标记,不该被面板样式盖掉)。
- 冒号跟着变红:.mv-lb::after / .dt-inline-lab::after 原本是灰色 400,
只改标签本体会剩「红字 + 灰冒号」的半截效果。
- 包裹层兜底(.field.is-req > label 等),只作用直接子级 ——
不写 .field.is-req label,那会把「提醒」那排 checkbox 的小标签一起染红。
- 深色主题给备用色 #F08A78。
- 保留 .mv-row > .mv-lb > i(万一有没清干净的星号不至于露灰星花,且静态断言会报出来)。
模板(16 个文件 / 30 处):
- _event_fields.php / _datetime.php 的标签闭包第三参数 $star → $req,
$dtLabelStar → $dtLabelReq,删掉死引用 $evLabelStar。
- 其余 14 个模板把 `<i>*</i>` 换成给标签加 is-req 类。
- 用显式 .is-req 而不靠 required 属性反推:CSS 无父选择器,:has() 旧 WebView
不可用,且影院名称/板块名本来就靠 JS 校验没有 required。
验证:
- 静态 _fb_v94_static.py 22/0,反向 9/9(含「--req-w 必须严格大于从 CSS 解析出的
标签基线字重」,不写死 700)。
- 真浏览器 _fb_v94_req.py 63/0,反向 5/5(加粗/必须红/!important 必要性/
星号必须消失/每形态单独覆盖,五类注入各自精确命中预期断言)。
- 全量流水线(含 60+ 套件)通过并部署。
- 线上真实页面 _fb_v94_live.py 31/0:观影「片名」、旅程「旅程名」、
成员「姓名」、导航「网址/所属板块」实测 color=rgb(176,58,46) weight=700,
同页非必填 color=rgb(44,35,29) weight=600,冒号同色,无星号。
|
||
|
|
f1b09a2f41 |
v93: 首页日历本下拉能拉了 / 日程浮层只留编辑删除 / 重复日程三档 / Markdown 标题四级阶梯补齐
用户两条: ① 「新建日程,日历本是拉不下来的,是空的,好像只有一个"工作"什么的」 ② 「点日程后的详情跟原来的弹框几乎一模一样,这个详情就没有意义了」 ⇒ 只显示「编辑」「删除」;重复日程的删除/编辑要有三档 (当前 / 从现在到以后 / 全部) 【① 根因】`_add_modal.php` 被日程页与首页共用,HTML 一样,但自绘下拉的整套 JS 只内联在 `templates/calendar/index.php` 底部 ⇒ 首页那份没有交互,点了菜单不展开; 而按钮上是服务端渲染的默认本("工作"),所以看着"只有一个工作"。 修法:抽 `assets/js/cs_picker.js`(全站 defer、按 `[data-cs-picker]` 初始化、 `data-cs-ready` 幂等)。不扫所有 `.cs` —— 观影表单的影院下拉有自己的内联绑定, 两边都绑会"点一次开两次"。 【②】浮层自定义日程不再显示「详情」(派生条目仍只有真链接「详情」); 删除/编辑三档措辞按用户原话;`occ` 为空时只给「删除全部」,绝不让空串触发整条删除。 编辑三档的入口在 `fb_entry_json()` 把 `occ` 拼进 edit 地址(`?d=`)—— 不带就只有"修改全部"一档。 【顺带修掉 v85 没做完的一半】全量套件在 v61 A9b 报红,查下去不是本轮改坏的: CSS 只把 `##`(h4)调到 17px,`###`(h5)留在 15.5px(正文 15px,只差 0.5px)、 `####`(h6)留在 14.5px(比正文还小),而那段注释自己写的就是"三级都要调大"。 阶梯重排 h3/h4/h5/h6 = 22/20/18/17,四级两两不同号。 更值得记的是四条判据全部只盯 h4(v61 A9b、v85 B2/B2b 前身、静态断言、线上 A4), 而 h4 恰好是唯一修好的那一级 ⇒ 各加"每一级/最矮的一级"判据,并用 「只把 h6 打回 14.5px ⇒ A9b 仍绿、只有 A9b2 变红」证明新判据有独立价值。 【工具链】`_b61_falsify.py` 的 main_all 改成每个 case 起独立子进程 —— 原先同一进程连跑 10 遍探针,Playwright 的临时 profile 建/删会在同一轮里累计, 撞沙箱批量删除钩子被 SIGTERM(症状:输出戛然而止、退出码 1、一条 FAIL 都没有)。 验证:v93 正向 28/0 · v93 反向 3 处注入各自被抓 · v61 34/0 · v61 反向 9/9 · v85 md_heading 12/0 · v85 静态反向 8/0 · 全量流水线通过并已部署。 |
||
|
|
a7b3d8cfd8 |
v85: 首页日程刻度按结束时间定范围 / Markdown 标题真正有层级
用户报了两处(网页版):
① 「今天的日程,有一个日程到下午五点半,首页日程刻度只有九点。」
真根因:掐头去尾只按 `time`(开始时间)算范围,`end_time` 压根没参与
⇒ 08:30-17:30 被当成"8 点半结束",endH = 8 + 2 = 10,
刻度只画到 09:00;色块高 540 分钟硬塞进 3 小时格子,被 min(96, …) 夹住糊成一片。
修法:收集「开始 + 结束」两组分钟数,endH 由 max(结束) 向上取整到小时;
保留"区间必须包含此刻"的原逻辑与下限保护。
② 「日记 markdown 代码都没解析,## 后面的是标题,现在显示的还是和正文一样。」
真根因:渲染一直是对的(`## x` → `<h4 class="md-h">`),问题全在视觉 ——
`h4.md-h` 是 15px,而阅读态正文 `.note-read-block` 也是 15px,字号完全同号;
标题只有 font-weight:500,肉眼看不出层级。
修法:h3/h4/h5/h6 分别 18/17/15.5/14.5px,父级 .md-h 字重 600、行高 1.45,
h6 用 muted 色;`:first-child` 去掉多余的顶部外边距。
反向验证:
_fb_v85_agenda_range.py 13/0(钉死 08:00 做 A/B + 真实 now 复核 5 条)
_fb_v85_md_heading.py 9/0(真实 Markdown::render + 真实 CSS + 计算值)
_fb_v85_static_falsify.py 7/0(部署静态断言 6 处注入各自都能变红)
线上真值复核(family.yangyq.net,真实库 #1853 工程管理硕士开题 08:30~17:30):
旧模板 刻度 07~09(3 行)下沿 10:00,色块 top=50%/h=50% —— 复现用户所见
新模板 刻度 07~17(11 行)下沿 18:00,色块 top=13.64%/h=81.82% = 95.46% 完整在车道内
CSS h4.md-h 17px / 正文 15px / .md-h font-weight 600
|
||
|
|
4b58fec796 |
v82: 首页卡片补「新建日程 / 新增成员」+ 两处红线
用户原话:「日程,没有新建日程按钮,名册,也没有新增成员这个按钮。 另外,首页上的日程,红色线不对,变短了左侧缺了一块。在日程模块中, 那个红线左侧是不是还有一个红点?如果有,去掉。」 首页卡片就地操作(不用进模块): - HomeCards.php 的 agenda/contacts 补 add + add_modal 元数据 - templates/calendar/_add_modal.php:从 calendar/index.php 抽出的 「新建日程」弹框片段(首页与日程页共用;片段自读变量一律加 cal 前缀, 避免 require 共享作用域覆盖宿主变量) - templates/people/form.php:加弹框形态(pfModalOnly / pfModal), 弹框形态下「取消」走 data-fb-add-close - index.php:首页 agenda/contacts 两个 $homeAddModals 分支; calendar/event_add 支持 back=home 白名单跳转(不采信任意 POST 字符串) 红线(左端缺一块 / 多余红点): - .ag-now 原来写 left: var(--ag-gutter,50px),但 v80 起它已经在 .ag-lines 内部(那层自己已有同样缩进)⇒ 左沿跑到 100px、比车道多 出一整份刻度宽。改成 left: 0 - .tg-now::before 整条删除(日程模块页那个 8px 红点) 涉及共用件(HomeCards/index.php),已跑全量:e2e 1572/0、 v82 正向 20/0、v82 反向 15/0、流水线全绿并已部署。 |
||
|
|
fd90e33d94 |
v81:首页卡片「新增电影」「新建纪念日」弹框(不离开首页就把事办了)
用户 2026-10-22:
「首页上的其他卡片也应该有这些基本功能。比如电影观影,它有一个"新增电影"的功能;
如果是纪念日,就有一个"新增纪念日"。实际上,就是把原来模块里弹的那个框,现在拿到
这边来。目的就是为了在首页上就能快速完成一定工作,而不用进到模块里面。」
做法:复用模块页**同一份**表单,不另写。
- app/HomeCards.php:TYPES 每张卡多登记 add(按钮文案)/ add_modal(弹框容器 id)。
- templates/home.php:按登记表挂 data-fb-add-open="#<id>"(没登记的仍用 v78 的
data-tk-add-open,两种写法并存但各有唯一来源);页尾 require 模块的表单模板。
- assets/js/add_modal.js(新):通用开关,只认 [data-fb-add-modal] /
data-fb-add-open / data-fb-add-close;幂等闸门;Esc 先让 antd 收面板;
打开那一刻才 boot 日期控件(hidden 时尺寸为 0,antd 会定位到屏幕左上角)。
- templates/{movies,days}/form.php:只在首页内嵌时($fbAddModalAttr)挂通用属性
并输出 back=home,模块页行为一字不变。
- index.php:days/save 与 movies/create 加 back=home 白名单(取 POST 再比字面量,
不把 POST 当跳转地址)。
过程中修掉两个真问题:
- 登记表第一版写 anAddCard,而 days/form.php 实际是 anNewCard ⇒ 按钮属性写对了、
DOM 也不报错,点下去毫无反应。已加「按钮指向的容器真的存在」判据与部署静态
断言里的交叉校验(拿登记表 id 去 templates/ 里逐个找 id="...")。
- 探针判「提交后回首页」原本读 pg.url,而 wait_for_load_state(networkidle) 在
点击那一刻本就 idle 时立刻返回 ⇒ 读到提交前的地址,恒绿。改成拦那次 POST 的
响应看 (状态码, Location),拆成 ⑭a 提交真发生 / ⑭b 302 指向 home / ⑭c 浏览器停在 home。
验证:正向探针 23/0 · 反向验证 14/0(七注入各自变红)· e2e 1572/0 · 几何 428/0。
|
||
|
|
b1d4502148 |
v80 首页日程:修「右侧缺一块」——色块百分比按含刻度的整宽算了
用户:「网页端,首页,日程好像显示不全,右侧是缺一块的,是不是左侧有刻度, 导致你算宽度的时候,没算对?」—— 猜对了,而且比想的严重。 原因:色块内联 `left: calc(46px + L%)` 是**绝对**让位(46px 给刻度), `width: calc(W% - 4px)` 的百分比却相对**含刻度的整宽**(.ag-grid 481px)。 于是 100% 时色块宽 477、左端 48,右端落到 744,而卡片右边界只有 700 ⇒ 多出 44px 被 .hc-agenda 的 overflow:hidden 裁掉,日历本名正好在那截里。 日历页 .tg-block 没这个病:它左边刻度是独立的一列(.tg-col),百分比天然相对列宽。 修法(换参照系,不是调数字):新增 .ag-lanes 定位层,左让刻度、右留 2px, 色块与红线都挪进去,百分比就相对真正可用的宽度算;刻度宽度收成单一变量 --ag-gutter(50px),以后改刻度只改它。内联删掉 46px。 验证四层: - _fb_agenda_geom.py 27/0:不越界 / 左右留白 2&4px / 宽度自洽 / 与日历页同公式 - _fb_agenda_pixel.py 8/0:截图数着色列,右沿留白 5px(修前 0px) - _fb_agenda_falsify.py 11/0:退回旧写法与车道超宽都变红,合法微调仍绿 - e2e 1558/0 · 几何 428/0 · b37 5/0 同时修两处失效探针: - _fb_b37_ui.py 点 #taskAddBtn(v78 已改成 [data-tk-add-open])导致基线超时全红 - _fb_b37_falsify.py 注入锚点写死 calc(46px + ...),改到会重现该 bug 的 width 上 部署脚本加 v80 三条静态断言(已逐条注入确认会红)。 |
||
|
|
40fd6c501c |
v78 服务端:首页任务改成「就地处理」面板 + 新建任务搬到首页
用户原话: 「网页端,首页点击一个任务后,除了展示信息,应该有相应的功能,可以修改状态: 待办、进行中,可以挪动分组,可以拖动上传附件,可以添加评论,添加子任务等。 其目地,是为了让用户不离开首页,就能对任务进行简单的处理。」 「另外,在全部任务旁边,放一个新建任务的功能,其实就是把任务中的新建和编辑功能, 搬过来,用弹出框来处理。」 一、首页任务浮层由「只读信息」改成「可操作面板」 - 新增 templates/tasks/_panel.php:状态三段 / 看板 chips(带看板色)/ 子任务(复用 _task_check.php)/ 附件拖放框(复用 _attachments.php)/ 讨论 - 新增路由 tasks/panel:只吐一块 HTML(找不到 404) - 既有七个动作路由各加一条 ajax_panel 回包分支(状态 / 挪分组 / 新建子任务 / 评论增删 / 附件增删),没有新开写接口 - helpers.php 新增 task_panel_wanted() / task_panel_json(); fb_entry_json() 的 payload 增加 panel 字段(只有任务会给) - 每次动作回**整块新面板**:一次动作会牵动子任务计数、附件张数、看板选中态, 逐处同步 DOM 必然漏一处 - 拖进来的附件**立刻上传**(任务已存在,不该再让用户点一次保存), 不复用 _stage_drop.php 那套「暂存 + 认领」 二、新建任务搬到首页 - 从 templates/tasks/index.php 抽出公共片段 templates/tasks/_add_modal.php (字段与原来一字未改),首页与任务页共用同一份 - 新增 assets/js/task_add.js 管开关(两页共用,幂等绑定); 打开那一刻才 fbUpgradeRange() + fbBootAntd()(hidden 时宽高为 0,antd 会定位错乱) - App\HomeCards 登记表里加 add => 新建任务,首页卡头与「全部任务」并排 - tasks/create 支持 back=home(白名单),新建完回首页 三、其它 - assets/js/task_panel.js:事件委托;动作后整块换面板; 顺手同步首页那张卡的状态徽标与复选框(关掉浮层不能还是旧状态); 传附件时保住用户写了一半的留言与子任务标题;取不到面板时退回只读信息 - app.css 追加面板样式(含 color-mix 的 @supports 兜底)与卡片窄到宽 2 时允许折行 验证:本地 e2e 1558/0、几何 428/0 + 660/0、v78 反向验证 11/0、 片段作用域体检 35/0、php -l 与 node --check 全过 |
||
|
|
8a9629650a |
v72:日历本显示与管理合并成一张清单,颜色改用内置调色模态框
用户 2026-10-21 两条:
「日历设置里面日历本显示和日历管理可以放在一起」
「预制的颜色点了之后没反应……选颜色那个地方点了之后是弹出 Windows 的
一个颜色选择框,这个不好,我们把它改成内置的」
改动:
· calendar/display 整页删除,并入 calendar/calendars(保留 302 老地址)
· 新增 templates/_color_picker.php(按钮 + 值载体,表单/独立双模式)
· 新增 templates/_color_picker_modal.php(全页唯一模态框,预置色 + hex 手输)
· 共享本提到同一清单,标注归属与权限;owner 校验服务端做
· 清掉 display.php 时代遗留的死 CSS(.cal-set-page / .cal-set-book* 等)
部署中抓出并修掉两处真问题:
· 部署脚本「清掉被取代文件」清单漏了 display.php —— tar 只新增/覆盖,
git rm 掉的文件在线上永远留着,被第 7 段断言拦下(断言是对的,缺的是删除步骤)
· _color_picker.php 里残留 <input type="color" class="cp-trigger">:
原生取色控件既不能有子元素、也没有 data-cp-open ⇒
按钮回显失效(找不到 .cp-trigger-dot/hex)+ 模态框永远打不开,
用户点到的还是 Windows 取色框。改成 <button type="button"> + 色点 + hex
(v72 探针在修复前实测 2 条红,修复后 50/0)
|
||
|
|
b45cf671d1 |
v71c:首页「通知消息」改成正规卡片,样式直接复用通知页
用户 2026-10-21:
「先把web端的首页修一下,通知消息这个,也是首页的卡片,可以调整顺序和大小。
另外,这个样式也不好看,我看个人头像点击后,通知消息那个页面的样式就不错,
直接复用就可以了。」
① 卡片化(HomeCards 注册表 + 默认布局 + 取数)
· TYPES 新增 'notif'(w4/h4,route=notifications,more=全部通知)
· DEFAULT_LAYOUT 钉在 brief 之后,与 days 并一行(宽 4)——
若插在 brief 与 agenda 之间,agenda/tasks 会被挤成一上一下(实测 C13 顶边 407 vs 751)
· data() 加 case 'notif' → 新增 notifData():最新 10 条 + 未读数,
与原来的取数口径一字不差;关掉卡片就不查这两条 SQL
· templates/home.php 删掉网格前那块写死区块(v30 第 5 条「必须在往年的今天前面」
这条约束作废 —— 那时还没有卡片体系);index.php 不再单独传 notifs/notifUnread
② 样式复用 templates/notifications/index.php
· 结构与通知页完全一致:头像在前 → 正文 → kind 图标在右(带 ntf-kind-{kind})
(旧首页那块是"图标在前、头像第二",正是用户说不好看的那一版)
· 新增 .hc-ntf 作用域只补"卡片比整页小"的三条差异:正文允许折两行(宽 2 时
单行省略会把正文全吃掉)、时间挪到正文下一行、未读数做小注脚
· 不带每条右侧「删除」按钮(4×4 的卡里塞两个按钮会把正文挤成两三个字),
删除在通知页做,卡片右上角有「全部通知」
③ 顺带修掉 v63 A1 抓住的一处:'notif' 的 mod 给 'home' 而不是空串
(通知不在 Module::LIST,但它是首页自己的卡片;空串会让「全部通知」成为
一排同色模块按钮里唯一白底 ghost 的那个)
⇒ TYPES docstring 立规矩:route 非空就必须有 mod,认不出归哪个模块就归 home
验证(familybook_push.sh 第 6 轮全绿,PUSH_RC=0,2h11m)
v62 首页卡片 56/0 · v62 反向验证 14/0(新增 M8–M12 五条注入)
b30 通知 16/0 · b30 反向验证 18/0
v63 40/0 · v63 反向验证 51/0 · e2e 1523/0 · 部署静态断言空跑 RC=0
同批带上上一轮 v70/v71 的日历修复(已单独验过,本轮流水线一并回归):
· v70 日历导入后条目不显示:+N 条从纯文本 span 改成真按钮,点开是当天全部
条目浮层;AJAX 新建后就地插入与服务端同形(回 entry,不让前端自己拼)
· v71 ICS 导入顺序:主事件 → 覆盖行 → EXDATE 由 Calendar::importIcs() 统一保证
|
||
|
|
b2b103df0f |
v68:修好日程「地点」的高德联想(v54 起静默失效)+ 账本四条动作路由
用户 2026-10-20:「网页端添加日程的时候,输入地点,应该调用高德接口,
获得准确的详细地址,现在这个功能不好用了,先修一下。」
## 根因不是 key、不是域名白名单 —— 是插件从来没被拉起来
`layout.php` 里 `fb-amap` meta 给的 SDK 地址**不带 `plugin`**,而加载器优先用它
⇒ `window.AMap.AutoComplete` **恒为 undefined** ⇒ 联想实例建不出来。
更糟的是 `addr_suggest.js` **一进函数就打 `data-addr-bound='1'`**,
于是"绑定失败"被记成"已经绑定过",此后**再也不重试** —— 全链路静默,
界面上就是"输了字什么也不弹"。
(用真浏览器逐环量过才定位:key 有效、`inputtips` 真能返回 10 条候选,
问题全在我们自己这一侧。)
修法(双保险):
- `layout.php` 新增 `needAutoComplete()`:SDK 到位后 `AMap.plugin('AMap.AutoComplete', cb)`,
超时也放行;启动顺序改成 **SDK → 插件 → 我们的脚本**。
- `addr_suggest.js` 就绪判据收紧成 `!!(AMap && AMap.AutoComplete)`;
`data-addr-bound` 改为 **new 成功之后**才打;新增 `ensurePlugin()`
并监听 `fb-amap-ready` 自愈(插件晚到也能接上)。
## 「准确的详细地址」
日程的地点**只有一栏**(`_event_fields.php` 只有 location),只把名字写回去,
回头要导航/找人还是查不到门牌。⇒ 地点框加 `data-addr-detail="1"`,
选中候选后写「名字 · 详细地址」(名字在前,人认的是名字),
并在这一行下面给一条「高德:省市区 · 门牌」提示。
⚠️ 提示行必须插在**整行之后**,塞进 `.mv-row` 会把 62px 标签列挤掉(v24 踩过)。
⚠️ 详细地址必须**延后一拍**写:AutoComplete 在 select 回调之后还会把 `poi.name`
塞回输入框,同步写会被它覆盖(第一版就因此白写)。
## 账本:服务端补四条路由
`Ledger::rename / setShare / unshare / shares / updateEntry / removeEntry`
**模型一直都有**,`ApiHandler::ledger()` 却只开了「建账本 / 记一笔 / 删账本」
⇒ 端上只能把「共享给谁」「账本名字」画成灰条,账目也改不了删不掉。
新增:`PATCH /ledger/<id>`(改名)、`/ledger/<id>/shares`(GET/POST/DELETE)、
`PATCH|DELETE /ledger/<id>/entries`(id 走 `Api::int('id')` ——
`Api::dispatch()` 只解析 3 段,`/ledger/3/entries/17` 的第 4 段会被丢掉)。
`Ledger::mine/find` 补 `is_owner`(`my_perm` 只有 read/write,分不出主人);
改共享只允许主人(422)。
## 验证
- `familybook_push.sh` 全量流水线跑到 v47 停在 1 条失败:
「③ 选中候选后地点被换成规范地名」—— 因为这条旧契约是 `loc == '万象城 CGV'`,
而我们现在**有意**写成「名字 · 详细地址」。
⇒ 断言**升级**(不是放宽):既要求名字在前、又要求门牌进来,比原来更严。
- `_fb_v47_addr_sim.py` 复验 **16 / 0**;
并做注入验证:把 `data-addr-detail="1"` 删掉 → ③ 立刻变红(证明断言真守着)。
- `_fb_v68_addr.py` 三模式 5/0(`--old` 拆掉两处时候选 0 条、`--one` 只拆 layout 仍 5/0)。
- 线上核对:`/api/v1/ledger/1/shares` 返回 **401**(不是 404,路由已生效)。
|
||
|
|
2f7dc6ca23 |
v63:卡片配色 / 日历本靠右 / 地图题图 / 设置页清理 / 公共标签颜色 / 令牌下线
- 卡片右上角按钮改用「来自哪个模块」的主题色:HomeCards 登记表新增 mod 字段 与 module(),首页从登记表取色(原来是模板里硬编码 in_array(['agenda','tasks']) 白名单,新加带按钮的卡片会默默变灰) - 首页日程把日历本放到色块右侧:.ag-ev 改弹性行,「时间+标题」包进 .ag-ev-main(flex:1 1 auto; min-width:0),日历本作右侧兄弟。 原来 display:block 会把日历本顶到第二行,色块一矮就完全看不见 - 旅程地图显示题图:服务端把 media_url(cover_key) 塞进地图数据, 地图卡片左侧 40x40 题图(object-fit:cover),没有题图不留灰框 - 系统设置第一块改叫「系统标语」;删掉「首页上那一行小字」提示、 「不是全家的统一口号」说明、「上次结构升级有步骤失败了」整框; 运行环境删「排错时看这一块」与「亲属连线」那一行(功能与数据未动) - 公共标签:说明文字全删;列表与编辑合并成一行(色块+名字+N人+箭头+编辑); person_groups 加 color 列(Schema addColumn,老库可升);点「编辑」弹框 改名字与颜色、点「完成」提交;下面那个独立编辑列表撤掉。删除挪进弹框 - 个人设置删掉「手动签发接口令牌」整块;settings/api_token 路由保留但挡下提交 (只删界面会留下知道地址就能用的后门);令牌只由登录后自动签发, 设备管理卡保留。apiTokens/apiPlain/apiLabel/apiBase 四个变量一并撤掉 |
||
|
|
9a66068c33 |
v62:纪念日列表修正 / 分类图标与设置菜单 / 首页卡片体系
- 纪念日列表:display:flex 从 td 移到内层 div(写在 td 上会破坏表格列算法, 表现为关联人列与其它列高度不一致);行内列序按表头对齐; 改完再点「新建」不再带出上次修改的数据;文案改为「与我相关」「来自名册」; 别人记的、把我挂成关联人的条目一并显示(split() 返回 hiddenIds, 路由层据此过滤「接下来的日子」,与两张表同一套可见性) - 分类图标:新增 app/AnnivIcon.php(17 个行内 SVG 图标 + 同 key 的 emoji 供日历纯文本处使用,内网无 CDN 不用 FontAwesome);去掉「看日历」按钮; 设置按钮改为菜单,第一项进 days/settings 独立页;分类可自选图标, 列表徽标与日历条目都按分类显示 - 首页卡片:新增 app/HomeCards.php 与 templates/home_cards.php; 8 格宽 × 高 2/4/6/8;首页拆成 templates/home/_card_*.php 共 8 个片段; home/cards 可改尺寸、调顺序、开关 |
||
|
|
4c63e42091 |
v61:笔记补齐成熟软件的块(Markdown 引擎加表格与任务清单,命令表扩全)
用户反馈:「笔记应该增加更多的块:段落、清单、图片、代码……去查成熟笔记
软件都分哪些块,我们都应该支持。」
对照 Notion / flomo / 思源 / Logseq 的块清单逐项核对(块类型仍只有
段落/图片两种——v29 用户拍板"清单并入 Markdown"),补齐的是
**Markdown 引擎与编辑器到 GFM 关键子集**:
app/Markdown.php
- GFM 表格:表头+分隔行两行联合识别(正文里有竖线不会误判成表格);
`:---`/`---:`/`:-:` 左右中对齐;列数向表头看齐(多丢少补,格子不错位);
单元格走 inline()(先 htmlspecialchars,`<script>` 只会被画成字面文本);
段落收集遇到"紧跟正文的表格"会断开(GFM 不强制空行)
- GFM 任务清单:`- [ ]`/`- [x]` 渲染成**只读**勾选框(笔记不是任务系统,
能点却不保存是假交互;要勾就改源码里的 x)
- 摘要清洗:表格分隔行/竖线/任务方括号不进列表卡片
assets/js/note_editor.js
- 命令表补齐:H1/H2/H3、有序列表、待办清单、删除线、行内代码、表格模板、
分割线(工具条与斜线菜单仍是同一份表)
- 🔴 修真 no-op:v29 的"逐行套"写着逐行实际原样返回,选中三行点列表只给
第一行挂前缀;现在行首记号型命令逐行套(空行跳过)
- 表格模板光标落在第一行数据的第一格,插完可直接打字
templates/notes/form.php:工具条 8→14 键(高频);全部命令斜线 `/` 可调
assets/css/app.css:.md-table / .md-task 样式(跟主题变量,不写死色)
刻意不做:代码语法高亮(引库=几 MB,用户当年已拍板轻量)、折叠块
(与"逐段编辑"打架,flomo 也没有)、页内子页/数据库(家书没有那个形态)。
验证:_fb_v61_ui.py 真机 30/0(含 XSS 夹具、紧跟正文断段、实时预览同源、
逐段编辑接口、v28 老语法零回退、历史 list 块不丢);
_b61_falsify.py 8 处注入 8/8 全红在预期断言;回归 b29 38/0、v55 30/0、v60 28/0。
|
||
|
|
54ab9f825b |
v60:范围时间控件只改一端(点「确定」不再跳回开始)
用户反馈:范围控件里前面的时间不用改,直接点后面的结束时间,选完之后它还是
跳回开始时间、要重新选开始 —— 没必要。
真根因(不是接线错,一个回调都不挂的裸 RangePicker 也一样):
① rc-picker 点「确定」会把活动端复位到「开始」(那是为下一次选择准备的,
正常情况面板同时也该关掉,用户看不见);
② 这个版本关完面板又把焦点送回开始输入框,而 Trigger 「聚焦即展开」,
于是面板又开了、还停在「开始」端。
修法:接管 open —— 点「确定」就关(onOk),开合由 onOpenChange 同步。
只给成对的范围控件(观影/日程/行程的起止)加;单点控件(任务截止时间)
不动 —— 它点确定本来就能正常关闭。
编号说明:v56/v57/v58/v59 已被「设备会话 / 增量同步」那批用掉,本批顺延 v60。
验证:
- _fb_v60_ui.py 真机 28 条(只改结束日期/时刻不动开始、确定后关闭、
关掉后能重开、点面板外能关、日历点格子带入时间仍同步、单点控件不受影响)
- _b60_falsify.py 3 处注入全部变红(不接管 open / 确定不关 / 连张开都吞掉)
- 回归:v55 套件 30/30、v45 重复到 10/10 仍全绿
|
||
|
|
190dc56301 |
v55:笔记详情不重复标题 / 任务状态选完即改(去按钮)/ 编辑任务弹框 / 子任务截止时间换最新控件
用户 2026-10-17 四条。
① 笔记详情:正文里不再重复显示标题
用户原话:「笔记详情里面的标题已经变成这个日笔记的题目了,那**下面那个里面
就不用再显示这个题目**了。」
背景:v52 已把**页头**改成笔记自己的标题(并参与 <title>),
而**正文区里又写了一遍**同一个标题 ⇒ 同一页同一个名字出现两次。
⇒ 删正文里那个 `<h1 class="note-read-title">`;**页头那份保留**。
连带删掉已无使用者的 `.note-read-title` CSS 规则
(留一条没有使用者的规则,只会让下一个人以为"正文里还该有个标题")。
② 任务详情:状态**选一下就改**(去掉「改」按钮)
用户原话:「任务详情里边,状态后面**不用加按钮**了。不是说选了之后再点一下才改,
而是**只要一选就让它改**:选一下,状态就发生改变就行了。」
⇒ `<select>` 的 `change` 直接 fetch 提交到**既有的** `tasks/status`
(它本来就认 `X-Requested-With` 并回 JSON —— 不新开路由、不加新契约)。
成功后**就地**同步三处(不刷页、不丢滚动位置):
· 下拉本身;
· 页头副标题的状态词;
· 页头那颗「标记完成 ↔ 重新打开」按钮的文案与 hidden 值
(改了状态而按钮还写"标记完成"就自相矛盾)。
反馈:一条**浮在顶部**的轻提示 `.tk-flash`(`position:fixed` 不占文档流 ⇒
改状态不会让页面跳动)。循环任务那句"下一次也排上了"(`d.msg`)照旧显示 ——
不能因为改交互把 v24 定下的行为丢掉。
⚠️ **无 JS 兜底**:`<noscript>` 里那颗「改」按钮
(规范:脚本开启时 noscript **整块不渲染**)。不要用 `<button hidden>`+JS
去"显示"它(会闪、脚本报错时永远不出现)。
🔴🔴 **踩坑(本轮最值钱)**:第一版写成"先 `sel.disabled = true`、再
`new FormData(form)`" ⇒ **HTML 规范:禁用的控件不参与表单数据构造** ⇒
字段丢了 ⇒ 服务端收到空 status ⇒ 落回 `todo`。
症状是"请求成功、回包 ok、页面还给了提示,唯独值不对" —— 极具误导性。
⇒ **先构建、后禁用**。
③ 编辑任务 = 弹框
用户原话:「**编辑任务,弹框处理**。」
原来详情页是个 `<details>` 就地展开表单(展开会把下面的子任务/附件/评论整段往下推);
现在点「编辑这项任务」**浮起弹框**(`.cal-modal`,与「新建任务 / 新建日程 /
编辑观影记录」同一套类名)。
⚠️ **表单一个字没改** —— 仍 require 同一份 `_edit_form.php`
(条目浮层 `tasks/edit?embed=1` 也用它,两处永远同一张)。
这次只加了个 `$tkEditModal` 开关:true 时套 `.cal-modal` 壳 +
「保存」挪到底栏(与新建任务一致:主要动作在右下角)。
⚠️ 打开那一刻才 `fbBootAntd()` / `fbUpgradeRange()` 升级里面的日期控件
(hidden 时也能挂上,但 antd 弹层定位需要元素有实际尺寸)。
⚠️ 事件委托挂 `document`(与观影详情页同一理由:校验失败若重画这块,委托仍有效)。
④ 子任务截止时间:换最新控件
用户原话:「**子任务的截止时间,使用最新的时间控件**。」
**真根因**:`tasks/show.php` 子任务那块 require `_datetime.php` 时
**漏传 `$dtPlugin`** ⇒ 空值时走**旧的三下拉(年/月/日)**
⇒ 与「新建任务」「任务编辑」里的 antd 控件**不一样**。
⇒ 补 `$dtPlugin = 'antd-single'`(截止时间是**单个时刻**,不是范围)。
⚠️ 全仓这一族的调用点**逐处核对过**:只有子任务这一处漏了;其余
(日程起止 / 观影起止 / 任务新建 / 任务编辑 / 行程起止 / 修正起止)
都已有 `'antd'` 或 `'antd-single'`。
⚠️ `people/form.php`(生日)**有意不传** —— 生日是"日期",年份要能下拉到 1900,
与"截止时间"不是一回事,别顺手改。
验证(新增 2 套件 + 更新 deploy 断言):
· _fb_v55_ui.py(**30 条**真机断言)
· _b55_falsify.py(**8 注入 / 8 全抓**,基线 30/0)
· e2e **1518/0**;familybook_deploy.sh 加「第55批形态」段(**13 条**)
· 部署 **0 FAIL**;线上 4 文件 md5 逐字节一致;
线上剥注释核实:笔记模板/`app.css` 里已无 `note-read-title`(反向)、
任务模板有 `data-st-select` / `fd=FormData` / `noscript` 兜底 / `antd-single`、
编辑表单有 `tkEditCard`/`tkEditSubmit`、`app.css` 有 `.tk-flash`;
诊断页已删复验 404
⚠️ 本轮踩坑(已写进 pitfalls.md):
· 🔴🔴 **先 `disabled=true` 再 `new FormData(form)` ⇒ 字段会丢**(表单数据不含禁用控件);
· 连带印证:**用裸 `strpos`/`grep` 做反向断言会命中注释**(我的线上诊断脚本
就因为这个连报两次"还有",剥注释后才对)—— 反向断言一律走"剥注释"。
⚠️ 另:工作树里 `app/ApiHandler.php` / `app/PersonQuery.php` / `app/Home.php` /
`app/Api.php` / `app/ApiToken.php` / `app/Schema.php` / `index.php` /
`templates/settings/me.php` 是**另一个会话**正在做的 App 端 / 设备管理改动,
一字未碰、也未提交。
|
||
|
|
8d79857f1a |
v54:首页任务标题截断(真根因=两列网格被撑破)/ 名册分组→标签且候选读词表 / 旅程地图视图 / 旅程设置按钮 / 显示方式默认列表或地图
用户 2026-10-16 傍晚五条。 ① 首页「近期任务」标题太长会挤成两行 用户原话:「首页**近期任务**那个地方要注意截断:如果任务标题太长, 会把这个任务**挤成两行**。」 🔴🔴 **真根因是两层,第一版只修了表面**: · 表面:`.dash-what` 只有 `flex:1 1 auto; min-width:0`,**缺** nowrap/hidden/ellipsis ⇒ 收缩后文字在框内换行; · 真身:`.home-duo` 是**两列 grid**,grid 子项默认 `min-width: auto` ("我最小也得装下我的内容")⇒ 一条长标题把那一列顶到 **1763px** (父 `home-duo` 才 1032px)⇒ **整个两列布局被撑破**、页面横向溢出到 **2089px** (视口 1440)。⇒ 行宽 1721px、`scrollWidth === clientWidth`, **`text-overflow: ellipsis` 看起来"没生效"** —— 其实容器自己先爆了。 修法:`.home-duo > * { min-width: 0; }`,且 **整条 flex 链**都补 (`.hd-pane` → `.hd-pane-head` → `.hd-pane-body` → `.dash-list/.tk-list` → `.tk-row`) —— 链上任何一层漏了,收缩就断在那一层、下面的 nowrap 全白写。 配套:任务标题加 `title=` 悬停看全。 ⚠️ 这是本项目**第二次**踩 flex/grid 的 `min-width:auto`(v53 的 `.mv-grid` 是同类)。 **以后"文字截断不生效"先量容器有没有被撑破**。 ② 名册「分组」→「标签」;打标签的候选要**从设置里读** 用户原话:「现在名册里面给人打标签的时候,**这个标签数据是从哪读出来的**呀? 在这个名册的设置页面,有很多分组,现在还叫分组。但是打标签的时候, **却不是从这些分组里读出来的**。……应该叫「名册标签」,是不是? **应该从这里面把所有标签都读出来**吧?……以后就没有分组的概念了, **全部都是标签的概念**。」 🔴 **用户观察得完全正确,这是两套数据源**: · 设置页显示的词表来自 `person_groups`(**分组表**); · 打标签的候选来自 `PersonTag::cloud()`(**你已经打过的词**,现算的); ⇒ 两边根本不是一个数据源 ⇒ "设置里列着一堆、打标签时却只有几个"。 修法:**`PersonTag::choices()` = 设置页词表 ∪ 我用过的词**(并集,词表在前)。 · 词表里的词**还没用过**也能一点就打上(这正是词表的意义); · 用过的词补在后面(历史不丢); · 名册列表 / 新增 / 编辑 / 挑人 / 导入 **五处同一个函数**。 界面口径全面改"标签"(**表名仍是 `person_groups`**:改名要背着 `PersonGroup` 类、 `person_roster.group_id`、以及**另一个会话正在写的 `app/ApiHandler.php`** (它引用了 `PersonGroup::orderFor`)一起动,风险远大于收益): · `_person_groups.php` 标题「名册标签」、按钮「加一个我自己的标签」; · 删除文案改成"只是从候选里去掉这个词,已打在别人身上的不受影响" (**语义真的变了**:标签是各自身上的字符串,不是引用); · `people/_settings_menu.php`、`people/settings.php`、`settings/me.php`、 `settings.php`(「公共分组」→「公共标签」)全部同步。 顺带按"全部都是标签"改另两条入口: · `people/pick.php`「加进哪一组」下拉 → **打标签**; · `people/import.php` 同上;`ContactImporter::run()` 加 `$tags` 参数。 🔴 新增 `PersonTag::addOne()`(**追加**一个标签)—— 这两处**绝不能用 `setFor()`** (它是"先 DELETE 再 INSERT"的整组替换)⇒ 会把人**原有**的标签清光 (症状是"用一次导入,别人原有的标签全没了",极难联想)。 ⚠️ `group_id` 在 pick/import 的**服务端仍收着**(老书签/脚本兼容),只是界面不再展示。 ③ 旅程模块加**地图视图**(读 TripLog 移植) 用户原话:「你去读一下 TripLog 的代码,在旅程这个模块,**可以在地图上显示旅程数据**。 1. 先读一下它之前那个代码是怎么实现的,**在我们的旅程模块也实现一下**。」 读 TripLog(`templates/activities/index.php`)得到的要点: · 用**高德 AMap**(合规白名单内,绝不是境外瓦片); · `position` 必须是 **[lng,lat] 数组**(传 `{lng,lat}` 裸对象会让 AMap 抛错、 整个绘制循环当场中断 —— 它注释里写着"批 11 的光秃秃地图就是它"); · `setFitView()` 后**上限 11 级**(只有一趟时会被放大到街道级,那是导航视角); · `.amap-marker` 整层 `pointer-events:none`(否则包装层挡住别家卡片的点击); · **容器必须有固定高度**(0 高度 → 空白地图)。 它那套"同城环形扇开"是**几百行几何**,本仓数据量没压出这个问题 ⇒ **有意不做**。 本仓数据更规整:`trip_nodes` 早有 `from_lng/from_lat/to_lng/to_lat`(v17 批) + `from_city/to_city` ⇒ 新增 `Trip::attachSpots()`: ① 有坐标取**重心**(不是端点 —— 端点会让"青岛→伊春"偏到某一头); ② 没坐标退到**城市重心**(`Geo::lookup` 离线城市表,**不发网络请求**: 列表页几十趟若逐条打高德逆地理,一次开页就是几十个外部请求); ③ 都没有就不上图(列表里照常有,模板会说清"有几趟没上图")。 🔴 城市那里**必须也取重心**:`cities()` 的顺序是**按时间**(出发地在最前), 取"第一个城市"会让**所有旅程都叠在家上**、地图毫无信息量。 布局:两个视图用**标签页**(`?view=map` 是**链接**不是纯 JS 按钮 —— 书签/前进后退/无 JS 都正确;也**有意不**把地图藏在 `display:none` 里, 高德在 0 尺寸容器里会渲染空白)。 高德 SDK 加载器重构:原来 `fbLoadAddr()` 顺带拉 `addr_suggest.js`, 而地图只用 SDK 自带的 `AMap.Map/Marker` ⇒ 抽出 `window.fbAmapSdk()`(只拉 SDK)、 新增 `fb-amap` meta(不带 plugin),`fb-addr` 那份共用同一加载器。 ④ 旅程列表页「新建旅程」**后面**加「设置」按钮 用户原话:「2. 在旅程的「新建旅程」后面也加一个「设置」按钮。」 ⇒ 补 `_settings_menu.php`(设置恒排最右),菜单**第一条**加「显示方式」。 ⑤ 显示方式:默认列表还是地图(用户自配) 用户原话:「3. 里面的设置让用户自己配置:**到底默认显示旅程列表,还是用地图显示**。」 ⇒ 新增 `trips/display`(设置页 + `?set=` 快速设置)与 `trips/display_save`, 存 **`user_settings.trip_default_view`**(每人一份,**不是**全家设置)。 ⚠️ 访客也能改(只影响自己看到的默认,不是数据变更)。 验证(新增 2 套件 + 更新 e2e/deploy 断言): · _fb_v54_ui.py(**33 条**真机断言) · _b54_falsify.py(**11 注入 / 11 全抓**,基线 33/0) · e2e **1518/0**;familybook_deploy.sh 加「第54批形态」段(**24 条**) · 部署 **0 FAIL**;线上 **20 个文件 md5 逐字节一致**; 线上实测:词表 24 条、`choices()` 24 条、**`cloud` 15 条全部 ⊆ `choices`**、 4 趟旅程全部可上图(样例落在济南·青岛重心);诊断页已删复验 404 ⚠️ 本轮踩坑(已写进 pitfalls.md): · 🔴🔴 **"文字截断不生效"要先量容器有没有被撑破**(第一嫌疑是 flex/grid 子项 `min-width:auto`,不是 `text-overflow` 写错); · **反向断言("不该再出现 X")必须用 `_code_has`(剥注释)** —— 需求原话正当留在注释里,裸 `grep -F` 会命中自己的说明(本轮部署假红一次); · **冗余防线会让反向验证打不红**(两处 `min-width:0` 只去掉一条 ⇒ 探针照旧全绿, 注入必须一起去掉); · **界面多加一个字符就可能打歪既有断言**(我在词表管理页给标签名加了 `#`, 把 e2e 的 `my_group_order()` 读成 `#家人`)。 |
||
|
|
26026c7250 |
v53:纪念日记年份算「第几个」/ 首页待办排在进行中下面 / 日程编辑浮层跨满整行 / 新增成员打标签 / 新增页一次填完称呼与备注
用户 2026-10-16 五条。
① 纪念日要记录年份,才能知道「第几个」
用户原话:「纪念日需要记录年份,这样才能知道**是第几个纪念日**。」
**真根因有两层**(两处一起才构成"没有年份"):
· 表里没有:anniversaries 只有 month + day(当年当成"一年一度,年份无意义");
· 表单有、但被丢掉:日期控件本来就带年份(<input type="date"> 是 YYYY-MM-DD),
而拆分 JS **只取 mm/dd**(`mo.value = m[2]; dy.value = m[3];`)⇒ 想存也没存上。
修法:anniversaries.year(SMALLINT UNSIGNED NULL)+ 升级路径 addColumn;
表单补 hidden year、拆分 JS 一并写;Anniversary::save 收 year(**区间守卫 1900..2100**);
occurrences() 输出 year 与 nth;列表页显示「2018 年起 · 第 9 个」。
⚠️ 年份**可空、且不给默认值**:老数据就是没有年份,猜一个(按创建年之类)
会让页面算出一个**错的**"第 N 个"。NULL = 只按月日过、不显示序号 —— 诚实。
⚠️ 顺带修一个自相矛盾:repeat_type='once' 原来落在公历分支 ⇒ **年年都冒出来**;
现在有年份了 ⇒ 只在那一年出现(没年份的保持老行为,不凭空丢)。
🔴 **序号算法抽成单点来源 `Anniversary::nthFor()`** —— `occurrences()`(临近区)与
days/index.php(我添加的那张表)**都调它**;两处各写一遍是"改一处忘一处"的温床。
用**今天所在的年**("今年是第几个"),不是"下一次发生那年"。
🔴🔴 **踩坑(最值钱)**:`addColumn`(ALTER TABLE)**必须排在那张表 CREATE TABLE 之后**。
第一版放进 movie_details 那段(anniversaries 的建表更靠后)⇒ 全新安装
`1146 Table 'fb_test.anniversaries' doesn't exist` ⇒ 整段 ensure() 抛异常 ⇒
**install 直接"安装失败"**(POST 回 200、不自删、schema_version=None、库一片空白)。
新建库走 CREATE TABLE、老库走 addColumn —— **两条路都要在**。
② 首页近期任务:把待办也放进来(排在进行中下面)
用户原话:「首页的近期任务,**把待办也放进来吧,放在进行中的下面**。」
原来只收「进行中 + **已逾期**的待办」(当年怕待办一多首页变长清单)。
⇒ 现在**全部**收进来、按**状态分组**(doing 在前、todo 在后);
两组内部仍按截止时间升序(byStatus 的 ORDER BY 已排好)。
⚠️ 配套改统计:`count($tasks) 件在做` 会把待办也算成"在做" ⇒ 改成**按状态拆开数**
(进行中→「件在做」、待办→「件待办」,两数相加=列表行数,仍同源)。
「件待办」原读 stat7['todos'](**与列表不同源**,v24 踩过"数字和列表对不上")。
⚠️ 空态:「没有进行中的任务,也没有待办。」
⚠️ .hd-pane-body 本来就是 overflow-y:auto + flex:1 1 auto/min-height:0,行数变多**内部滚动**、
卡片高度不变(v32"两块外框恒等高"那套兜住了)。
③ 日程编辑浮层「只有左侧有内容,右侧空白,好像分栏了」
**真根因**(assets/css/app.css):日程编辑页 event.php 里,`.mv-grid`(观影式**纵向**列表)
被嵌在**两列**的 `.form-grid` 里 ⇒ 它只占**第一列**。整页形态下看不出来(页面宽),
但**浮层里面板只有 620px** ⇒ 两列各 286px ⇒ 所有字段挤在左边 286px、右边整列空着。
实测:panel=620 / form-grid=588 / cols="286px 286px" / mv-grid w=286
→修后 mv-grid w=588(95% of panel)、标题输入框 w=516、距右缘 ~2px。
修法:`.form-grid > .mv-grid { grid-column: 1 / -1; }` —— **一行 CSS**。
⚠️ 不去改 event.php 里"整页保留两列、浮层用 mv-grid"那个结构判断(那是有意保留的);
`.mv-grid` 本身就是"一整块、占满宽度"的语义,天生不该被塞进半列。
⚠️ 排查手法:这种"看起来像分栏"的问题**量几何比读 CSS 快** ——
getBoundingClientRect 打出 panel/grid/cols/内层宽,一眼看出"嵌了几层 grid"。
④ 添加成员时是**打标签**,不是选分组
用户原话:「名册中添加成员时,**不是选择分组,而是打标签**。现在已经没有分组的概念了,
**只有标签的概念**。」
⇒ 新增分支的「所属分组」下拉(group_id)换成**标签输入**(复用 v51 的 #ptBlock 那套:
候选勾选 + 自由输入,**文本框是唯一提交源**)。
⚠️ 只在**新增**时出现在那个位置(编辑时上面那个 #ptBlock 已在管标签,出现两块会让人以为填两遍)。
⚠️ **分组表结构 person_roster.group_id 保留不动**(名册"拖动归档"还在用、历史数据也在),
只是**新增入口不再要求人选**。
⚠️ e2e 里 v14「新增表单**有**分组下拉」与新需求正相反 ⇒ 改成「没有 group_id + 有 name=tags」。
⑤ 添加成员页一次填完「我对他的称呼 + 我的备注」
用户原话:「我在添加一个成员的时候,是不是可以直接在添加成员的页面里填写**我对他的称呼**
以及**我对他的备注**,**一次就解决**?要不然还得先添加、再返回他的名册去填,有点麻烦。」
⇒ 新增页也出现这两栏:
· **我对他的备注**(person_notes)—— 条件从 `$isEdit && !$isSelf` 改成 `!$isSelf`;
· **我对他的称呼**(nicknames)—— **新加的输入框** `name="my_nick"`。
服务端 people/create 落两表(挂**我**名下:viewer=我, target=他)。
⚠️ 判"表单里有没有这一栏"用 **array_key_exists**(不是判值是否为空)——
与编辑路径同一口径:**空串是明确的"清空"语义**(Nickname::set('') 会删行)。
⚠️ 四处**重渲染路径**(撞名确认/校验失败/手机号重复/people/new 初始)都要带上
myTags/tagChoices/myNick,否则一撞名或一报错,刚打的标签与刚写的称呼备注就全没了。
⚠️ 编辑页也补 myNick 回显(否则"新增时填了、编辑页看不见"会以为没存上)。
验证(新增 2 套件 + 更新 e2e/deploy 断言):
· _fb_v53_ui.py(**26 条**真机断言)
· _b53_falsify.py(**10 注入 / 10 全抓**,基线 26/0)
· e2e **1517/0**;familybook_deploy.sh 加 v53 静态断言段(14 正向 + 1 反向)
· 全流水线 **PUSH_RC=0**、部署 0 FAIL(含"第53批形态")
· 线上 8 文件 md5 逐字节一致;`anniversaries.year` 已由升级路径补上(nullable smallint);
线上实测 nthFor 4 用例全对(2018→第9个 / null→NULL / 2019→2 / 未来年→NULL);
诊断页已删并复验 404
⚠️ 本轮踩坑(已写进 pitfalls.md / 工作日志):
· **`addColumn` 必须在同表的 CREATE TABLE 之后**(否则全新安装 `1146` ⇒ ensure() 抛异常);
· **探针里 `dispatchEvent(new Event('submit'))` 不会真的提交表单**
(不导航、不发请求)⇒ 判"存没存上"必须真点提交按钮或 `requestSubmit()`;
· **反向验证的 `restore()` 不能只是"再跑一遍 build_work()"**(指纹闸门会把它变空操作
⇒ 第二轮基线是脏的,26/0 掉到 14/12);恢复必须**无条件** copy2;
· **注入锚点在源码里出现两次 ⇒ 打到别处 ⇒ 那条断言恒不红**
(v53 给新增页也加了 `id="ptBlock"`,打偏了 v51 那条"编辑页标签块"的注入);
· **别把两个共用 fb_test 库的套件并行跑**(互相删播种行 ⇒ 看着像"整套功能坏了");
· **部署静态断言的锚点引号/反斜杠必须与源码一字不差**:`\[` 在 bash 双引号里是
**字面反斜杠**、单双引号不能混、漏个 `(int)` 也搜不到 —— 一轮里连红 4 条全是这个。
|
||
|
|
ed122c034b |
v52:旅程 CalDAV 结束时间 / 笔记(标题·段落下加段加图·逐段就地编辑)/ 名册自动匹配与设置页 / 模块设置整合表
用户 2026-10-15 七条。
① 旅程行程经 CalDAV 同步后「结束时间 = 开始时间」
**真根因**(app/Calendar.php 的 exportRows()):对**派生条目**(观影/旅程)写死
'ends_at' => null,而 Ics::vevent() 遇到空结束会退化成「DTEND = DTSTART」
⇒ 手机上每条行程的结束时间都等于开始。
**附带第二个 bug**:分组键含日期(kind-ref_id-date),而 month() 把**跨天条目逐天**
铺出来 ⇒ 跨天节点被拆成 N 条(UID 相同),resources() 按 UID 去重后**只留第一天**,
而第一天那条没有结束时刻 ⇒ 又回到 DTEND=DTSTART。
修法:键改成 `kind-ref_id`,先收集同节点的各天,再用**首末天**拼一条真实跨度
(末天那条才带 end_time)。实测单日 DTEND=12:30、跨天 DTEND=第3天11:00,
每节点恰好 1 条 VEVENT。
⚠️ 副作用(已处理):v31 DAV 去重的反向验证原来靠「跨天节点逐天出、UID 全同」造重复,
本修把那条素材**修没了** ⇒ 给夹具换成「同一份 .ics 导两次」(两条 UID 相同的
自定义日程),守卫仍能被证伪(见 _fb_dav_diag.py 与 pitfalls 的同族条目)。
② 笔记详情页标题是"看笔记"而不是笔记名
阅读态 h1 改用笔记自己的 $noteTitle;**编辑态仍用「改笔记/写笔记」**
(编辑时标题就在下面第一个输入框,两处重名看着像重复)。浏览器 <title> 同步。
③ 加段落/加图片要在**输入框下面**
note_editor.js 的 makeBlock() 里每块下方加一排 .note-block-add(+段落/+图片),
点了插在**这一块后面**;顶部那组**保留**(用户明确"上面和下面都保留")。
⚠️ 挂在 body 里而不是 wrap 上 —— .note-block 是 display:flex 横排,
挂 wrap 会变成"输入框右边的第三列",不是"下面"。
⚠️ 用 data-add-below(不是 data-add):顶部是"追加到末尾"、块内是"插在本块后"。
④ 笔记详情页「逐段就地编辑」(分块多时不用跳编辑页找那一块)
模板给每块包 .note-read-item[data-block][data-kind][data-raw] + 右上角「编辑」按钮;
新增 assets/js/note_inline.js(按需加载):划过出现 → 点编辑就地变 textarea →
保存 POST notes/block → 只换这一块。
服务端 Note::updateBlock():**按块主键改一行**(不是整组替换)——
⚠️ 整组替换会换掉每个块的 id(前端手里的 data-block 当场失效)+ 并发互相覆盖;
⚠️ SQL 必须带 `AND note_id = ?`(块 id 全局自增,少了它就能改别人笔记里的块)。
权限与整条编辑同一把尺(find() + canWrite())。
⑤ 删掉「从已有家里人中挑选」,改成「新增/导入时自动匹配」
新增 Person::matchExisting():手机号(有 phone_hash,一条 SQL 精确命中)→
身份证(无哈希,整表解密比)→ 姓名(**先记下不立刻返回**,等精确命中看完再退到它)。
people/create:校验通过后先 matchExisting,命中就在**同一张表单里**出确认条
(#personConfirm)。⚠️ **不做成单独确认页**:单独页要把整张表(含工作经历/补充联系
方式这些**数组字段**)原样回传,漏一类就静默丢数据;同表单里字段天然全在。
⚠️ 判"是不是第一次提交"用 array_key_exists('confirm_existing')(0=不是、1=是、无=首次)。
导入那条路的缺口(本轮补):ContactImporter 命中已有的人时只补字段、**不挂进导入者的
名册** ⇒ "文件里有他、系统里也有他,但导完我的名册里看不到他"。已在命中分支加
PersonRoster::ensure(..., $meId, ...)。
顺手加 PersonRoster::has()(避免调用方又踩"visibleIds 是值列表不是集合"那个坑)。
⑥ 名册页只剩「添加成员 + 设置」;分组搬到名册设置
templates/people/_settings_menu.php(新):☰ 下拉两行 = 导入联系人 / 名册分组。
templates/people/settings.php(新,路由 people/settings):require 从个人设置**整块搬出来**
的 templates/_person_groups.php。
⚠️ 是**真搬**不是复制(两份一模一样的分组界面,改一处忘一处是必然的);
⚠️ 片段变量一律加 pg 前缀 —— 模板片段与宿主 require 是**共享作用域**,
用 $groups 这种通名会覆盖宿主同名变量(本轮真踩到,已改)。
⚠️ 表单 action 仍指 settings/groups/*(一个字段没改)。
⑦ 个人设置:模块设置整合成一张表
原来是**三张卡三个表单**(首页模块 / 导航模块 / 主题色),同一串模块名出现两遍。
现在一张表五列:顺序 | 模块 | 导航 | 首页 | 颜色,一次保存。
新增 settings/modules_save 路由:**底层存储一个没改**(仍是 nav_modules /
home_modules / module_colors),只是把三个 save 依次调一遍 ⇒ 老数据天然兼容。
⚠️ 权限**没有**因为"合并"被放宽:颜色仍是家庭一份、只有管理员能改
(非管理员那列是只读色块;提交时服务端静默忽略 c[])。
窄屏:表格 min-width:560px + 外层 overflow-x:auto(375px 实测横向溢出 0)。
验证(新增 5 套件 + 更新若干旧断言):
· _fb_v52_ics.py(3)/ _fb_v52_note.py(19)/ _fb_v52_roster.py(12)/ _fb_v52_mods.py(13)
· _b52_falsify.py(**12 注入 / 31 断言全抓**)
· _fb_import_test.py 补 ⑯(命中的人也要回到导入者名册);_fb_dav_diag.py 换去重素材
· e2e 1516/0;_fb_batch_e.py / _fb_e2e.py / familybook_deploy.sh 里随 #6/#7 搬家或改形的
旧断言(分组在个人设置、模块行 li→tr、show_present、RRULE 前缀)意图一字不改
· 全流水线 **PUSH_RC=0**、部署 0 FAIL(含"第52批形态");线上 16 文件 md5 逐字节一致
⚠️ 工程纪律(已写进 pitfalls.md / 工作日志):
· 探针自起的 PHP 服务**一律随机空闲端口**(写死 + "连上就复用" 会静默测到别的目录/库);
· **同一工具链目录 + 同一库名不支持两个会话并发** —— 本轮最后一个多小时的"诡异全红"
全是**另一个会话**在同时跑 _fb_e2e.py、共享 fb_test 与 _fb_site 造成的;
· 并发下**连 git stash 都不能用**(会把别人的半成品一起藏起来),基线对照用 git worktree;
· 修好业务可能让"守着它的反向验证"空转(素材被修没了)⇒ 换一处等价素材,别删守卫;
· 断言盯**形态**而非具体数值(分栏那次写死 50% 在别的批次数据上假红)。
|
||
|
|
4fbac180e1 |
v51:旅程编辑保存后刷新 / 名册编辑页可改标签 / 手机号归一 / 反复点击带入时间+默认1h+分栏 / 日程设置拆分
用户 2026-10-15 六条。 ① 旅程详情「编辑」保存后页面不刷新(改了题图/标题,页头纹丝不动) **两个 bug 串在一起**: A. 只换 `#nodeListHost`(行程列表),而标题/题图/时间城市都在那容器**外面**(服务端直出); B. 🔴 `form.id === 'tripForm'` **永远为假** —— HTML 表单把**同名具名控件**挂成自己的属性, `tripForm` 里有 `<input name="id">` ⇒ `form.id` 返回的是**那个 input** (`String(form.id)` = `[object HTMLInputElement]`)。同类还有 form.name/action/method。 ⇒ 修法:`form.getAttribute('id')` 判型;tripForm(编辑旅程本体)→ **整页刷新**, nodeForm(行程增删改)→ 仍只换行程列表(那是 v41 用户明确要的"不整页跳")。 ② 名册编辑页不能选标签("应该能编辑这个人的所有信息才对") `templates/people/form.php` 里压根没有标签 UI ⇒ 新增 `#ptBlock`: 候选勾选(PersonTag::cloud)+ 自由输入(`name="tags"`,唯一提交源,勾选框只是快捷方式)。 ⚠️ `PersonTag::setFor(家庭, **我**, 他, …)` —— owner 永远是"我",不影响全家的标签。 ⚠️ 守卫按**字段存在与否**分流(array_key_exists),不是"值是否为空"——否则改自己时 表单没这个字段,按"空值=清空"会把人家的标签抹掉。 ⚠️ 校验失败的两条重渲染路径也要带上 `myTags`/`tagChoices`(取 `$_POST['tags']`)。 ③ 导入的手机号带空格 → 编辑时被判"格式不对"("统一处理一下") 真根因:主手机号正则 `^\+?[0-9\-]{6,20}$` **空格一律判错**,而补充电话那条收 `[0-9\- ]` ⇒ 同一个号码填主字段报错、填补充字段通过。 ⇒ ① `Person::normPhone()` 去空格/短横/括号/**全角空格 U+3000**; ② 校验改成「先归一再判纯数字」——⚠️ 只把正则放宽到含 `\s` **不够**: PCRE 的 `\s`(无 /u/UCP)**不认全角空格**,通讯录导出里真有; ③ create/update/replaceContacts **入库前归一**(原来 hash 内部归一但**密文存原样**); ④ `Schema::ensure` 加**一次性迁移**(哨兵 `phone_normalized_v51`)把已入库的 带分隔符号码解密→归一→写回(手机号是加密存的,SQL 里没法 LIKE/REPLACE)。 ④ 周/日视图"只有第一次带入的时间是对的" 真根因(真机给 `__fbRemount` 装 getter/setter 才抓到):起止字段藏在弹框里 ⇒ boot 时 `upgradeRange()` 因"两端不可见"提前 return ⇒ 那一刻**只有 mountAntd** 给它赋值 (会"重读原生值+重绘 antd");而**一打开弹框** `fbUpgradeRange()` 补跑 ⇒ 在尾部把它**重新赋成"只对齐原生下拉、不碰 antd"**的一版 ⇒ 此后 antd 控件永远停在第一次的值。 ⇒ `upgradeRange()` 不再降级覆盖更强的实现,改成"把对齐下拉串在它前面"(`__fbRangeSync` 防自套)。 ⑤ 默认结束 = 开始 + 1 小时;新增日程要按分栏错开(不再叠盖) · `setStart()` 里顺带设 ends_at = +1h(⚠️ 跨午夜顺延到次日;⚠️ 不捆进 fbSetDT, 那个接口被"编辑已有日程"复用,强改会弄丢用户填的时长)。 · 真根因:`putTimed()` 只把自己那条**整宽**铺上去,不知道同列已有日程 (服务端整页渲染有分栏,但那是整页重绘时才算的)⇒ 前端 `relayoutColumn(col)` **照抄服务端 `conflictLanes`** 重排整列;模板加 `data-s`/`data-e`(**声明的起止分钟**, 不能从 top/height 反推 —— 那是"实际显示结束",不是真实结束分钟)。 ⑥ 日程设置下拉太长(日历本一多就把下面的设置挤出屏幕)→ 拆成"四行列表 + 显示设置独立页" 新建 `templates/calendar/display.php` + 路由 `calendar/display`(默认视图 + 显示哪些本子); `_settings_menu.php` 从"内联表单"改成**纯列表四行**(显示设置/日历本管理/订阅管理/DAV 口令); ⚠️ `.cal-set-books` 原本 `max-height:42vh`("塞在下拉里"的妥协)⇒ 用 `.cal-set-page` 作用域覆盖,否则整页里再套一层内滚 = 要滚两次。 保存仍走既有 `calendar/prefs`(一个字段没改)。 验证:`_fb_v51_ui.py`(25 条真机)+ `_b51_falsify.py`(**12 注入 / 25 断言全抓**); e2e 1511/0;`_fb_batch_e.py` 与 `familybook_deploy.sh` 里"设置菜单含 show_present"等 旧断言随 #6 搬到 display 页(意图一字不改);全流水线 PUSH_RC=0、部署 0 FAIL(含"第51批形态"); 线上 10 文件 md5 逐字节一致。 ⚠️ 排查纪律(已写进 pitfalls.md): · `form.id` 被同名控件遮蔽 —— 判型一律用 `getAttribute('id')`; · 反向验证的注入可能"打偏"(改的那处不是探针实际走的分支)—— 本轮 12 个注入有 2 个一开始没变红,都是注入打偏、不是断言弱。 |
||
|
|
252812ed93 |
v50:旅程题图裁剪修好(AJAX 片段漏绑)/ 修正表单换最新控件+取消 / 打标签误报修复 / 名册按钮编辑蓝删除红
用户 2026-10-14 四条。
① 旅程详情点点「编辑」,里面的图片改不了、拖入/选中都不弹裁剪框
用户原话:「应该弹出来一个让我裁剪的那个框。但是现在却没有那个裁剪的框。」
**真根因**(assets/js/app.js):裁剪那段 IIFE 是**首屏一次性**的 ——
`var inputs = querySelectorAll('[data-crop-ratio]'); if (!inputs.length) return;`
在脚本加载那一刻扫一遍,扫不到就整段放弃。而「编辑旅程」是 **AJAX 取片段、
innerHTML 塞进弹框**的(templates/trips/show.php 的 loadInto)⇒ 详情页首屏
**没有**题图框 ⇒ 当场 return ⇒ 片段里的框**从来没有 change 监听**
⇒ 拖入/选中什么都不发生。
修法:拆成 `bindOne(input)`(幂等,打 `data-crop-bound` 标记)+ `scan(scope)`;
首屏调一次 + **MutationObserver 补挂**;库的按需加载改成"首次真要用时才拉"。
⚠️ 与「拖动上传」那段(bindInput/scan/MutationObserver)**同一手法** ——
它早就解决了"动态插进来的文件框也要接得住",裁剪这段当年漏了这层。
② 旅程「修正」表单(templates/trips/_revise_form.php)
用户三条:「开始和结束时间…应该改成我们最新的控件样式」「上面记录实际执行情况
那一段话说明也不要了」「记录这次修正的按钮应该放在右边,另外应该加一个取消」。
· 起止换 `$dtPlugin = 'antd'`(与 node_form.php 同款;当年 v41 刻意没换,现按用户要求统一)
· 删 show.php 里 `.rev-tip` 整段
· 按钮行 `rev-foot` 靠右 + 新增 `[data-rev-cancel]`;取消由 show.php 的**事件委托**
处理(`details.rev-open` 收起)—— 不用内联脚本(片段会被 AJAX 换进来)
③ 名册打标签误报「这个人不在你的名册里」
用户原话:「我搜索出来一个人…给他打标签,它会显示说这个人不在我的名册里,
其实他是在我的名册里的。」
**真根因**(index.php 的 people/tags):`isset(PersonRoster::visibleIds(...)[$pid])`,
而 `visibleIds()` 返回的是 **id 值列表** `[2,3,4,5]`(键是 0,1,2,3)——
把值列表当 id→值 映射用。只有 `$pid` 恰好等于数组下标时才为真:名册里人少时
(下标<pid)**谁都判"不在名册"**;一般情况只有"第 pid+1 个"碰巧过。
修法:改用 `in_array(..., true)`(与同文件 `inMyRoster` 写法一致)。
⚠️ **没改 `visibleIds()` 的返回形状** —— 另一处调用已是正确的 in_array,改形状会
静默弄坏它。陷阱:`PersonGroup::visibleIds()` 是集合、`PersonRoster::visibleIds()`
是值列表,**同名不同形状**,已在两处注释里写明。
(附带确认:不在名册的人本来就不出现在列表里,用户"不在名册就该搜不出来"本就成立。)
④ 名册列表按钮:编辑蓝底、删除红底
用**作用域类** `.person-edit`(var(--blue)) / `.person-del`(var(--red)) 三处形态都写,
不动全局 .btn-edit/.btn-danger 的语义(v41 踩过"裸类改色波及影院管理页")。
hover 在这里补 `filter: brightness(.92)`(文末"全站悬停统一"段是白名单写的)。
验证(新增 2 套件 + 更新 1 处旧断言):
· _fb_v50_ui.py(22 条真机断言:题图绑定/选中与拖入都弹裁剪框 16:9、修正 antd+无说明+
靠右+取消能收起、名册内 3 人打标签均成功且不误报、编辑蓝删除红)
· _b50_falsify.py(**10 注入 / 21 断言全抓**)
· _fb_e2e.py 的 ★第28-6条断言随 v50-4 升级(原查 var(--brand-soft),现查蓝+红作用域类)
· familybook_deploy.sh 加 v50 静态断言段(含两条"旧写法必须已不在"的反向断言)
· e2e 1511/0、几何、各批反向验证全绿;**v50 四条 22/0**、**v50 反向验证 21/0**;
全流水线 **PUSH_RC=0**,部署脚本 0 FAIL(含"第50批形态");线上 6 文件 md5 逐字节一致;
线上门禁诊断 16 项全 YES,临时页已删并复验 404
⚠️ 收尾时发现并修掉一个**自设陷阱**:我在 show.php 的删除说明里**整句引用了**用户点名要删的
那句原话 ⇒ 朴素 strpos 判"已删"时会命中我自己的注释(线上诊断 C8=NO)。
部署断言用 `_code_has`(剥注释)所以是对的,但这类字串留在注释里迟早坑到别的检查
⇒ 已改成不整句照抄。**改注释也改了文件哈希 ⇒ 又重跑一次全量部署**。
⚠️ 流水线末段唯一"失败"同 v49:沙箱安全删除门禁(按轮次累计删满 50 就终止进程);
单独重跑 _fb_v46_carddav.py = 29/0 ⇒ 环境护栏,与本次改动无关。
|
||
|
|
1bf56b114b |
v49:条目浮层的「详情/编辑/删除」按**来源**分形态(三颗靠右 + 就地展开/编辑 + AJAX 删除)
用户 2026-10-14 第 2 条。用户原话:
「下面三个按钮,都放在下面的右侧…分别是详情、编辑和删除。详细用蓝色,编辑用黄色,删除用红色。
点击详细的时候,**如果这是一个日程**(正常日程里的事件、不是来自观影/旅程等模块)
→ 就**在当前页面让模态框变大**、显示更多信息。
点击编辑,也是**在当前的界面**弹出编辑日程的框,就地编辑;编辑完之后下面两个按钮:取消和保存。
点击删除,就直接把这个日程给删除。
如果当前这个日程**来自其他模块**(观影 / 旅程 / 其它)→ 点击详情就**进入那些模块**展示它。
**这种情况下,下面就没有编辑、也没有删除,就只有一个详情。**」
拍板:① 应用范围 = 首页日程 + 日程页条目(首页近期任务保持 v43 现状);
② 重复日程删除 → 弹三档范围(这一次 / 这一次及以后 / 全部)。
实现(7 个文件):
· app/helpers.php fb_entry_json() 扩契约:source('event'/'derived') / ref / del / info /
repeat / occ。判据 `kind==='custom' && ref_id>0`(**不是**"有没有 edit")。
· app/Calendar.php month() 给自定义条目带 _can_write(供 del 判定)。
· templates/_entry_popup.php 底栏重构成「右侧按钮组 + 7 颗」:详情态三颗、
编辑态两颗(取消/保存)、删除范围态两颗(取消/确认删除);另加一颗
**遮罩层里的 CSRF**(正文会被整块换掉,放正文会丢)。
· assets/js/entry_popup.js 四态状态机(detail/info/edit/delscope),按 source 决定显哪几颗。
· assets/css/app.css .fb-entry-acts(margin-left:auto 靠右)+ fb-a-detail/edit/del 三色
+ .is-wide(620→860px)。⚠️ 全是**作用域类**,不动全局 .btn-edit/.btn-danger 的语义。
· index.php 新增 calendar/event?embed=detail(只吐只读详情块);
calendar/event_del **认 X-Requested-With 回 JSON**($done() 收口,失败也回原因)。
· templates/calendar/event.php 支持 $evDetail(只吐 kv 只读块,与可写与否无关)。
本轮修掉两个真 bug(都是"藏不掉"):
① `.btn{display:inline-flex}` 是作者样式,**盖过 [hidden] 的浏览器默认**
⇒ 底栏四态切换时前面的按钮收不起来、全挤在一条上(派生条目本该只有「详情」,
实测三颗都露着)。⇒ 显式写 `.fb-entry-acts .btn[hidden]{display:none!important}`。
本项目文件前部已有同款注释(.pick-row[hidden] 等),属第 N 次同类。
② `footShow('detail')` 把三颗无条件点亮,**盖掉** paintDetail 按来源设的 hidden。
⇒ detail 组只列「详情」,且 paintDetail 里**先 footShow 再设 edit/del**(顺序写进注释)。
验证(新增 2 个套件):
· _fb_v49_entry.py(33 条真机断言:三颗靠右/配色/派生只有详情/详情就地变宽/编辑就地/
删除落库/日程页入口/重复弹范围)
· _b49_falsify.py(**10 注入 / 21 断言全抓**)
· 更新 _fb_b43_ui.py 旧断言(「详细信息」→「详情」;自定义日程的详情语义从"跳页"改"就地展开")
· familybook_deploy.sh 加 v49 静态断言段
· 全量 e2e / vCard / 几何 / 各批反向验证全绿;部署脚本 0 FAIL(含"第49批形态");
线上 7 个文件 md5 与本地逐字节一致;线上门禁诊断 13 项全 YES,临时页已删并复验 404
⚠️ 反向验证抓到一条**假绿**(值得记下来):注入「给派生条目也塞 del」时探针**没变红** ——
因为按钮显隐是 `source` **且** `del` 的多条件"与",只塞 del 在界面上显不出来。
⇒ 补一条**数据层**断言(直接读 data-fb-entry 的 del 必须为空),两层判据都在。
⚠️ 末段唯一"失败"是沙箱的安全删除门禁(SAFE_DELETE_BULK_CONFIRM_REQUIRED,按轮次累计、
本轮流水线删超 50 个文件):单独重跑 _fb_v46_carddav.py 是 **29/0**,与环境有关,与本次改动无关。
|
||
|
|
6ee5662854 |
v48:日历本共享面板提速(×日历本数 → 唯一一份)/ 点日历带入开始时间真修 / 去十字光标
用户 2026-10-14 三条。
① 日历本共享弹框「特别慢」—— 真根因 = 选人组件 ×日历本数
用户原话:「设置日历本共享的时候,弹出框特别慢,好像又是 bitwarden 的问题,
需要再彻查一下。」
线上门禁诊断页实测(真实登录态):
cals=12, roster=800, pp_components=12, html_kb=5876.5,
json_attr_kb=1586.8, noscript_kb=4137.8, inputs=9657, labels=9593
服务端只花 ~70ms ⇒ **慢在浏览器侧**。本地夹具造出同规模复现:
首屏 4086ms / 整页 9.3MB / noscript 文本 6.7MB。
真根因:calendars.php **每本日历**都内联一整套 _person_picker,每个组件把
**全部 800 人**化成 1 份 data-pp-people JSON + 1 份 <noscript> 兜底 ⇒
12 本 = 12 份 ⇒ 近万个表单控件,浏览器/密码管理器逐个扫描。
⚠️ **不是 Bitwarden 独有**(用户怀疑它)——任何遍历 input / 读整页 HTML 的扩展都会中招。
修法:与账目页 #ledPanels / 名册 #rosterMenus 同一套(同类问题第 3 次)——
行上只留 [data-cal-share-open] 按钮,真表单全页唯一一份 #calSharePanel。
线上复验:html_kb 5876.5 → **526.6**、pp_components 12 → **1**、noscript 6.7MB → 0.34MB。
② 点日历带入开始时间失效 —— 真根因是 window.fbSetDT 被提前 return 吞掉
用户原话:「之前这个功能是有的,后来我们改了时间控件之后,这个功能就没有了。」
两层真根因(都在 assets/js/app.js):
· fbSetDT 定义在 `if (!hasMonth) { … return; }` **之后**(IIFE 尾部)——
日程页首屏没有 input[type=month] ⇒ 直接 return ⇒ 尾部赋值**永不执行**
⇒ setStart() 里 typeof window.fbSetDT === 'function' 为假。(与 v42 修
fbBootAntd 是同一家族:对外接口写在提前 return 之后 ⇒ 静默失效。)
· bindParts() 初始化是"以三个下拉为准"(rebuildDays(true); sync();),而
__fbSyncParts 这个**赋值**写在它们之后 ⇒ 点击带入的 dt-date 被旧下拉盖回。
真浏览器调用栈实测:set date=2026-10-10(setStart)紧跟 set 2026-10-14(sync←bindParts)。
· 第三层:group.__fbRemount 是 v35 的空壳,且 fbSetDT 查找被 data-semi-range-done
门住 —— 日程「结束」默认隐藏 ⇒ upgradeRange 早退、永不设该标记 ⇒
**可见的 antd 控件永不重绘**(隐藏字段对了、用户看到的还是旧值)。
修法:① fbSetDT 提到所有 return 之前;② bindParts 初始化改成**以权威字段 date
为准**(box.__fbSyncParts(); sync();,且赋值顺序提前);③ antd RangePicker/单点
控件改**受控**(value+状态)、__fbRemount 接成"重读原生值+重绘"、查找不再要求
data-semi-range-done。真浏览器实测:**可见控件**显示 2026-10-10 09:00 / 周视图 15:00。
③ 去掉十字光标
用户:「现在鼠标放在视图上时,变成了一个十字。我感觉这个没必要。」
删 app.css 的 cursor:crosshair 规则;`.rt-writable` 类名**保留**(它是"能不能写"
的 DOM 标记位,_fb_batch_e.py 断言依赖它)。点击建日程的能力**一字不动**。
验证(新增 3 个套件 + 1 个反向验证):
· _fb_v48_cal.py(12 条,含"**可见的** antd 控件显示值"两条)
· _fb_v48_picker.py(9 条,造 12 本 + 800 人规模,量首屏耗时/整页体积/picker 数)
· _fb_v48_share_e2e.py(4 条,共享真落库 + 权限正确)
· _b48_falsify.py(**6 个注入 / 13 条断言全抓**,含"antd 改回非受控"、
"不接 __fbRemount"、"重现第二份选人组件")
· 全量 e2e 1510/0、vCard 40/0、几何 428/0、v38/v39/v40/v47 反向验证全绿
· 全流水线 **PUSH_RC=0**;线上四文件 md5 与本地逐字节一致;
线上门禁诊断复验三条修复全部 YES,临时诊断页已删并复验 404
顺带修掉的**工具链自身缺陷**(详见 pitfalls.md):
· familybook_deploy.sh 有 8 处 `_code_only … | grep -q`,在 `set -o pipefail` 下
会因 SIGPIPE(141) **假红**(本轮缩短了 calendars.php 上半段就触发)。
新增 `_code_has <file> <str>`(先落盘再 grep,不走管道)全部换掉。
· v40 那条"数据丢失级"静态守卫盯的是老字面量 `rebuildDays(true)` —— 改成
接受两种安全形式,行为层最终判据仍在 _fb_b40_movie.py(全绿)。
· 更新 4 个反向验证脚本里因本次重构而失配的锚点(b38/b39/b40 + 新增 b48)。
|
||
|
|
32e9e9ab84 |
v47:地址联想三处接入 / 读书页慢的真根因(会话锁)/ 笔记两处 / 日程状态下线
用户 2026-10-13 七条。拍板:影院只记地址、不存经纬度(后续用高德反查);
日程状态只删界面与展示、数据库列保留(「后续会有一个整体清理的工作」)。
①🔴 读书页慢 —— 真根因是**会话锁**,不是封面(我先判断错了方向)
用户原话:「读书现在还是非常慢,就是进入读书这个页面,刚开始还是非常的慢。」
线上实测(真公网、同一会话连打 3 次):
第 1 次 TTFB 0.08s ← 触发微信读书同步
第 2 次 TTFB **8.73s** ← 用户说的"刚开始非常慢"
第 3 次 TTFB 0.07s
插桩拿到的服务端时间线把因果钉死:
21:18:05.483 QUEUED ← 第 1 个请求排队
21:18:05.501 FINISH 2ms ← 响应确实提前发出去了(这步是好的)
21:18:14.674 SYNC 9.2s ← 但它仍占着这个人的会话文件锁跑完 9.2 秒
21:18:14.682 QUEUED ← 第 2 个请求此刻才真正开始执行
⇒ **`fastcgi_finish_request()` 只断开客户端连接、不释放 PHP 文件会话锁。**
后台同步要 9.2 秒(真打微信读书接口),同一个人的下一个请求一路等到它跑完。
修法(缺一不可):
· `WeRead::queueBackgroundSync()` 回调里 **先 `session_write_close()`、
再 `fastcgi_finish_request()`**(顺序不能反);
· `index.php` 两处"渲染前排同步"的入口排完**立刻放锁**
(shutdown 回调不一定执行:客户端中断/服务器超时才不放过);
· 顺带修 **`Holiday::queueBackgroundFetch()` 的同型问题**(联网 12 秒)。
为什么以前没发现:v29 引入"后台化"时只验证了"响应是不是提前发出去",
**单次请求测量永远看不到这个 bug** —— 必须同一会话连打两次才现形。
线上复验:修复后连打 4 次 = 0.15 / 0.15 / 0.09 / 0.15 秒(修复前第 2 次 8.73s);
后台同步仍正常(`weread_sync_at` 已更新)。
② 封面:顺手也修了(但这不是用户说的那个问题)
· `media.php` 读 OSS 前先放会话锁(原来从不放,35 张封面串行);
· 新增 `Storage::cacheLocally()` 磁盘二级缓存(原来 `read()` 只读不写回);
· 让 304 不再先拉整张图(原来先取回 body、再判 `If-None-Match`)。
线上实测:冷取 149ms(走 OSS)→ 热取 35ms(本地缓存、字节一致)
→ 带 `If-None-Match` 返回 **304 / 19.8ms / 空体**;35 张封面 103ms → 20ms(5 倍)。
③ 地址联想:抽成全站共用一份(`assets/js/addr_suggest.js`)+ 接入三处
· 影院管理页、观影新增页的「新建影院」弹框、日程地点、行程出发/到达;
· `layout.php` 按需加载(`[data-addr]` 出现才拉 SDK + 我们的脚本),
并暴露 `fbLoadAddr(scope)` 给 AJAX 片段(v42 踩过"内联 script 不执行");
· 安全密钥在 SDK 之前设置;回填支持 `data-addr-fill` / `data-addr-for`、
**只补空白字段**(不覆盖用户手写)、直辖市 `province==city` 去重;
· 字段名有两套(影院 `address`/`place`,行程 `from_addr`/`to_addr` 缩写 `_addr`),
`fieldFor()` 带同义词表 + 后缀匹配;
· 影院**只存地址、不存经纬度**(按用户要求)。
④ 笔记两条
· 「点保存没反应」真因:附件区是个 `<form>` **套在保存表单里** ——
HTML 不允许 form 套 form,浏览器丢掉内层标签、保留它的 `</form>`,
于是**保存按钮成了孤儿**(`button.form === null`),点了不提交也不报错。
真浏览器实测:修前 `btnInsideForm:false` / 点后 POST=0;修后 true / POST=1。
改法:附件区改 `<div id="noteAttForm">`,上传按钮用 `formaction`。
· 详情页宽度:`.note-read { max-width: 820px }` → `none`
(内容卡 820px vs 下方共享卡 1032px;修后两者都是 1032px)。
⑤ 日程状态下线
· 删表单里的「状态」下拉、`Calendar::statusWord()`、浮层 JSON 的 `status_word`;
· **保留** `events.status` 列与 `Calendar::statusOf()`(契约不动,等整体清理)。
验证(新增 5 个套件):
· `_fb_v47_addr.py`(接线 24 条)/ `_fb_v47_addr_sim.py`(真浏览器 + 假高德,16 条)
· `_fb_v47_readperf.py`(缓存与会话锁,23 条)
· `_fb_v47_notes.py`(真浏览器,9 条)
· `_b47_readperf_falsify.py`(反向验证 9 个注入)—— **9/9 全抓住**
· 全量 e2e **1510/0**;vCard 规格+往返 40/0;TB 探针 44/0;
v44 反向验证 17/0;全流水线 **PUSH_RC=0**(含"第47批形态"静态断言)
· **线上终验**:读书页整页 81ms、封面 103→20ms、media 冷 149→热 35→304 19.8ms、
修复前第 2 次请求 8.73s → 修复后 0.15s、地址联想 meta[fb-addr] 带真高德 key
顺带修掉三处**探针自身**的假红/假绿(详见 pitfalls.md):
· `_b44_falsify.py` 的"整小时"注入在**整点**与正确实现等价 ⇒ 换成恒错值;
· `_fb_b44_ui.py` 3b 的并发计时断言在单进程 `php -S` 下必然假红 ⇒ 改断言确定事实;
· 多处探针的"清理删除"撞沙箱删除门禁 ⇒ 探针**中途死掉**看起来像产品坏了
⇒ 统一改成 `_safe_rm()` / 不删。
|
||
|
|
075ac6bbeb |
v44 第 44 批:界面 5 条 + 移动端 API v1 全量
界面(用户 2026-10-12 第 1~5 条) 1. 新建日程「日历本」并入 .cs 自绘下拉,颜色画进候选项,三字与下拉同行。 2. 新建日程「开始/结束」改「日期范围:」,删旧日期控件; 重复相关三行仅选重复时出现;删「(含第一次)」与「这段时间算忙碌」。 3. 条目浮层加「地点」、去掉「状态」行;首页红线按分钟定位(原只取整小时); 去掉左侧红点只留红线。 4. 任务页删掉状态说明提示(STATUS_HINT 三值清空)。 5. 新建任务:标题在前树框在后;截止时间改最新日历控件;负责人默认自己; 删冗余提示;「上面要做什么」→「任务名称」;清理括号说明。 移动端 API(用户第 6 条:导航模块除外,其余全量) - 新增 Bearer 令牌鉴权(api_tokens 表,Schema v32),与登录口令 / DAV 口令三套分立。 - Api.php 内核(鉴权/取参/信封/分页/分发 + _method 覆写)+ ApiHandler.php 17 条资源 + ApiToken.php + api.php 入口。 - 路由:router.php / .htaccess / index.php 三处 /api/ 兜底(部署脚本自动追加线上规则)。 - 个人设置新增「App 接口令牌」卡;docs/API.md 接口文档; docs/API-QUESTION-LIST.md 自主决策与待确认缺口清单。 验证:本地 e2e 1510/0、v43 真机 33/0、v43 反向 9/0、 v44 真机 41/0、v44 API 63/0、v44 反向 19/0;全流水线 PUSH_RC=0。 |
||
|
|
8c651b43eb |
v43 第 43 批:六条反馈(界面 5 + 条目详情浮层)
用户 2026-10-11 反馈六条,逐条定位真根因后修复: ① 纪念日「日期」多出一个控件 —— 只保留最新的那个 两层根因:(a)原生 `input[type=date]` 当初没包在 `.dt-native` 里, v42 那条"AntD 挂上后收起原生框"的选择器根本不命中;(b)更要命的是 `.dt-native` 基类写着 `display: contents` —— 该元素**没有盒子**, 给它 position/width 完全无效 ⇒ 必须把 display 显式改回 block 才收得起来。 ② 编辑纪念日把底层列表页弄没 —— 「修改」原来是整页跳 `days/edit` (套 layout = 一张只有弹框的空白页)。改为 `?embed=1` 只吐弹框那一块, 列表页 JS 取回来替换 `#anNewCard` 并打开 —— 与「新建」同一观感。 内嵌形态标 `$anModalOnly` 以免打 standalone 标记(关闭时不再跳页)。 ③ 拖图裁剪的层被弹框盖住 —— `.crop-overlay` 是 z-index:100 而 `.cal-modal` 是 200 ⇒ 裁剪层被整块盖住。提到 260(与 v41 的 .cn-modal 同一处理)。 ④ 行程弹框里"参与的人"弹不出选人框 —— person_picker.js 只绑定一次, 而弹框表单是 AJAX 片段 ⇒ 里面的 `.pp` 从未被 init。三件事一起: 暴露 `window.fbInitPickers()`(幂等)+ layout 暴露 `window.fbLoadPicker()` (脚本是按需加载的)+ 塞完片段后补调初始化。 ⑤ 日程新建排版/控件/文案 —— 改走观影式 `.mv-grid`(标签对齐 + 冒号); 起止补上 `$dtPlugin='antd'`(试点正式推广到日程);标签「时间范围」→「开始 / 结束」; 删三句提示(结束留空 / 多选提醒 / 全天)。 ⑥ 条目详情浮层(首页日程 / 首页近期任务 / 日程页条目)—— 点条目不再直接跳页,弹浮层显示详情;「详细信息」跳该来源详情页; 「编辑」就地换成该来源的编辑表单(四处 `?embed=1` 片段)。 新增 `templates/_entry_popup.php` + `assets/js/entry_popup.js` + `fb_entry_json()`(三个入口共用一个编码函数)。 验证:php -l 157 文件全过;div 深度 33 组合全配平;片段体检 20 片段无越界; 本地 e2e 1510/0;v43 真机 33/0;v43 反向验证 8/0;v38 36/0;v42 33/0; b31/b32 反向验证 8/0 与 5/0;部署断言本地空跑 RC=0;完整流水线 PUSH_RC=0。 |
||
|
|
81dfacaf1b |
v42 第 42 批:十一条反馈(界面 9 条 + CalDAV 两处新根因)
CalDAV(用户「还是不行,thunderbird 提示找不到日历」): - 🔴 `.well-known/caldav` 原来**只回 301**。Thunderbird 建账号时常是 **带鉴权直接 PROPFIND** 这个地址(把它当 DAV 服务入口探),而它对这一跳的 301 **不跟随** ⇒ 判定"这里没有 CalDAV" ⇒ 找不到日历。 ⇒ 按方法分流:带鉴权 PROPFIND → 直接 207 给发现内容(principal / home-set / 日历本清单,一步到位);其余仍是 301。carddav 同修。 - 🔴 `Location` 由**相对** `/dav/` 改成**绝对 URL**(按真实 HTTP_HOST 拼, 协议从 HTTPS / X-Forwarded-Proto 取)。TB 在服务发现这一跳对相对跳转不可靠。 - 线上终验:匿名 GET 301+绝对URL;带鉴权 PROPFIND 直接 207(含 principal/home-set); Depth:0 只回自身;Depth:1 列出 12 本日历。 界面九条: - ① 新建旅程不再带入旧数据(真根因:`_list_body.php` 的 `foreach ($rows as $t)` 循环后 `$t` 残留,而 `form.php` 读的正是 `$t` ⇒ 弹框被填上列表最后一趟的数据)。 修:循环变量改 `$row` + `form.php` 契约兼容(`$tIn` 优先)。 - ②③④ 行程弹框里的 AntD 时间控件 / 出行方式联动车牌与车次 / 高德地址联想全部失效 (真根因:AJAX 片段里的内联 `<script>` **既不执行也不保留**)。 ⇒ 加 `runInlineScripts()` 把片段内联脚本抠出来执行; 另修 `app.js`:`if (!hasMonth) return` 的短路让 `window.fbBootAntd` 从未被定义, 把它提到所有 return 之前。 - ④b⑤ 标签改「照片」「时间范围」。 - ⑥ 简介补"还没有简介,点这里写一句"的占位(简介本身一直在,是那条 summary 为空)。 - ⑦ 资源编辑复用新建弹框(`#resAddCard`),并在其底栏补「共享」「附件」入口 (第 7 条改动后这两个能力一度无入口)。 - ⑧ 纪念日「月/日」→「日期」+ AntD 纯日期控件(新增 mountAntdDate / [data-antd-date]), 存储契约仍是 month + day。 - ⑨ 编辑纪念日弹框关不掉(关闭逻辑原来只在 days/index.php,而 days/edit 是整页) ⇒ 搬进 days/form.php 自包含。 验证:e2e 1510/0;v42 真机 33/0;DAV·TB 35/0;DAV 反向 8/0; b23 55/0;b28 39/0;b32 26/0;b32 反向 5/0;v31 DAV 28/0;b31 反向 8/0; 部署断言空跑 337/0;完整流水线 PUSH_RC=0。 |
||
|
|
7544fb9f6a |
v41 第 41 批:流水线提速 + 12 项界面/交互开发 + DAV 彻底修复
DAV(用户「务必彻底解决」): - 真根因是 home-set 层级错:CalDav::HOME_PATH 原为 /dav/calendars/、 CardDav 原为 /carddav/addressbooks/,而日历本/通讯录的 href 在 /dav/N.ics/ 与 /carddav/N.vcf/ —— home-set 下面一本都没有。 Thunderbird 按 home-set 前缀认成员,于是「找不到日历本 / 找不到联系人」。 改为 /dav/ 与 /carddav/(所有既有 URL 一字不变,老配置不作废)。 - 又发现并修掉 Depth:0 违反 RFC 4918:原实现无论 depth 都列出全部子集合, 且第一条 response 是日历本(不含 current-user-principal)。改为按 depth 分流, Depth:0 只回资源自身、Depth:1 才列出子集合。 界面与交互: - 观影:新建影院弹窗层级修正 + 详细地址;照片左右按钮悬停不再位移; 弹框底栏改「确定 / 取消」。 - 旅程:新建旅程改弹窗并按观影式排版;详情页四个动作按钮按用户指定顺序与配色; 编辑与新增行程弹窗化(单容器 + AJAX);行程时间控件改 AntD(精确到分); 参与人默认带上自己;车次号/车牌号紧跟交通方式词。 - 资源:新建/编辑改弹窗 + 观影式排版 + 新建时即定共享人。 - 纪念日:新建改弹框 + 观影式排版。 验证: - e2e 1510/0;v41 十二项真机 48/0;DAV 反向验证 8/0; 部署断言空跑 337/0;模板 div 深度三形态穷举 33 组全配平。 - 线上 DAV 终验(--resolve 真实入口 + 真 DAV 口令)12/0; 线上确认 Depth:0 恰好 1 个 response。 |
||
|
|
f9e30bd7d8 |
v40 第 40 批:观影九条 + 修掉一个「编辑一次就把观影时间弄成 NULL」的数据丢失 bug
用户原话(九条):
①「观影详情页面左上角的观影记录,应该和观影的列表页面里面保持一致」
②「观影详情页面的编辑,还有返回观影的按钮,应该和观影列表页面里面的记一场电影保持一致」
③「删除按钮就用红色背景颜色」
④「观影详情里面的编辑按钮应该也是弹出的,和新增是一样的,编辑完以后,页面动态更新」
⑤「影院管理部分,注意控制列的宽度…最右侧的操作、编辑和删除两个按钮,已经被挤成两行了,
尝试让编辑和删除排在一行,宽度给它留够」
⑥「编辑使用蓝色的背景,删除使用红色的背景」
⑦「高级搜索里面的日期范围,也使用我们最新的控件」
⑧「观影统计里面,如果这个人在名册里面,但是他一部电影的记录也没有,就不要在下面的这个
按观影人统计里面出现了,太长了」
⑨「观影统计里面,月份分布和星期分布,12 个月用 12 种不同的颜色,星期 7 天用 7 种不同的颜色」
🔴🔴 **本轮最重:修掉一个数据丢失级的真 bug**(v37 引入、**影响所有 `.dt-range` 页面**)
· 现象:任何带已存时间的编辑表单,只要 JS 跑起来,日期在用户**什么都没动**时
就被抹掉 ⇒ 提交后库里 `starts_at` 变 **NULL**。
· 根因:`assets/js/app.js` 的 `bindParts()` 首屏写成 `rebuildDays(false)` ——
那个参数是"保留已选日吗",传 false 会**清空"日"下拉**;紧接着的 `sync()`
看到 y/mo 有值而 d 空 ⇒ 算出 '' ⇒ **把隐藏的 `*_date` 写成空串**。
· 修:改成 `rebuildDays(true)`。
· ⚠️ 为什么一直没被发现:e2e 是**服务端**驱动的(直接 POST 字段、不经过 JS);
真机探针只验"控件可见可点"与"改值能写回",**没验"什么都不动时值还在不在"**。
⇒ 补了回归断言 + 反向验证(把 true 改回 false 必须红)。
· ⚠️ 影响面大于观影:**日程 / 任务 / 旅程**的起止同样走 `.dt-range`,
修在共用件等于顺手修了四个模块。
① 详情页头部与列表页统一:`<span class="sub">观影记录</span>` → **`<h1>观影记录</h1>` + sub=片名**。
②③ 详情页「编辑」「返回观影」用**裸 `.btn`**(= 列表页「记一场电影」的模块品牌实心);
「删除」用 `.btn-del`(**红色实心**,原来是淡红描边的 `.btn-danger`)。
⚠️ 用户那句"编辑用蓝色"是**影院管理**的(见 ⑥),详情页要的是"与列表页一致" ——
第一版理解错、把详情页编辑也刷成蓝色,**反向验证当场抓住**。
④ 详情页「编辑」改**弹框** + AJAX + 保存后**动态更新**:
· 内嵌**同一份** `form.php`;新增 `$mvModalForce`(`$isEdit` 也强制弹框壳)
与可显式传的 `$mvOpen`;新增 `View::renderPartial()` 供校验失败时把弹框整块重画回 JSON。
· 保存成功:`fetch` 跟 302 → **只换正文容器**(保留滚动位置),并重跑页内内联 JS。
· 🔴 实测踩到并解决的四件事:
1. 模板里裸类名 `Person::roster()` 会**致命**(全局命名空间)⇒ 页面从此中断输出、
且不报给用户 ⇒ 弹框整块消失。必须 `\App\Person::`。
2. `<button form="mvForm">`(表单外的提交按钮):点它真的提交,但挂在 `document` 上的
`submit` 监听**收不到事件**(实测 `XRW=(无)`、`resource_type=document`)⇒ 整页跳走、
红字看不到 ⇒ 主路改成 **click 委托**(preventDefault),`submit` 只兜回车。
3. `innerHTML` 换正文容器后要重跑内联 JS,但**绝不能重跑带 `src` 的外部脚本**
(app.js 等,会二次绑定);本段脚本自己要有**幂等闸门**。
4. flash 是**一次性**的(`take_flash()` 读完即清)⇒ 成功后**不要**再取一次页面,
直接用第一次的 `r.text()`(否则"保存成功却没有任何提示")。
⑤⑥ 影院管理:`.cn-table { table-layout: fixed }` + 操作列 **132px** +
`.cn-ops { flex-wrap: nowrap }` ⇒ 编辑/删除**并排一行**;
新增语义色 `--blue: #2F6FB0`,编辑=蓝实心、删除=红实心
(⚠️ 必须在**页头动作区那一层**也写一条:`.btn` 的特异性会盖过基类)。
⑦ 高级搜索的日期范围换 AntD:新增 `[data-antd-daterange]`(**纯日期**版,
不传 showTime),与观影"时刻"那对共用同一套按需加载 + 中文 locale 逻辑。
⑧ `MovieStats::byPerson` 加 `HAVING cnt > 0`(⚠️ 不能写 `WHERE cnt > 0`:
聚合结果在 WHERE 阶段还不存在)。
⑨ 新增 `MovieStats::MONTH_COLORS`(12 色)/ `WEEKDAY_COLORS`(7 色)+
`monthColor()` / `weekdayColor()`;**统计页"月度趋势"与年度报告"月份分布"同月同色**
(用户没要求,但不一致一眼就看得出)。
验证(全部真机 + 反向验证):
· _fb_b40_movie.py **41/0**(九条逐条 + 两条 🔴🔴 日期不丢);
· _fb_b40_falsify.py **10/0**(日期抹掉 / 编辑按钮配色 / AJAX 主路 三处各自证伪 + 复原回全绿);
· 全路由 e2e **1502/0**(顺带修掉一条**依赖夹具数据**的脆弱断言:
"统计页必须有 bar-chart" → 无数据时改判"要么有图表、要么有空态");
· 页头按钮探针 **60/0** + 反向 **3/3 全部抓到**(删除按钮换成 `.btn-del` 后,
判据与注入目标都已同步更新);v37/v38/v39 各套全绿;
日期控件真机 53/0 + 反向 7/0;观影照片 16/0 + 8/0;DAV 三套;部署静态断言空跑 —— 全绿。
· 线上 11 个关键文件 SHA256 与本地**逐字节一致**。
⚠️ 顺带:`show.php` 的 JS 注释里写了字面 `<script>` / `<main>`,绊倒了我自己"按标签
提取内联 JS 做语法检查"的脚本(**与 v37 那次 `<form>` 同类**)—— 已把注释里的
标签字面量全部改写。⇒ 通识:模板注释里不要出现字面标签名。
|
||
|
|
f8b1b96fd7 |
v39 第 39 批:观影弹框(antd 中文化 + 影院搜索 + 简介备注各占一行 + 海报照片对齐)
用户原话(四条):
①「观影模块的时间控件,比如说时间呀,还有下面的确定按钮啊。现在显示的都是英文,
应该给它设置成中文。」
②「影院那个地方应该有一个搜索的功能,下拉的地方上面有一个搜索筛选的功能。」
③「简介和备注不要放在一行,也就是说一个字段它就占一行。所以简介后面是一行,
备注后面是一行。」
④「海报和观影照片为什么和上面的标题不一样呢?海报的字体和简介是一样的,也带一个冒号。
然后把后面点击或拖动进行上传往后放,和简介后面的输入框保持一样大小。观影照片也是
类似的,观影照片也是加粗。和简介的字体保持一致,不带冒号,后面放上点击或拖动进行上传。」
① antd 中文化 —— 三个条件缺一不可(缺任一条按钮就还是英文)
· 🔴 **英文的真根因**:`antd.min.js` 这个产物**不含 locale 包**
(实测 `window.antd.locales` 是 undefined)⇒ 只能用它内置的英文默认值。
**换文件**才解决,不是加参数 ⇒ 改用 `antd-with-locales.min.js`
(多带各语言包,全局多挂 `antd.locales`,`antd.locales.zh_CN` 即简体中文包)。
· 🔴 dayjs 的中文 locale 是**单独一个文件**(核心包只有 en)⇒ 引 `dayjs-zh-cn.js`
并调 `dayjs.locale('zh-cn')`(星期/月份由 dayjs 出)。
· 🔴 必须用 `<ConfigProvider locale={zh_CN}>` 包住控件 —— antd 读的是**上下文里的**
locale,不包的话**日历文字会变中文、按钮却仍是英文**(最容易误判成"已经中文化了")。
· 独立探针 `_tooling/_antd_probe/{zh.html,zh_probe.py}` 9/0 把这三条钉死。
· 删除不再引用的 `assets/vendor/antd/antd.min.js`(1.5MB 冗余,留着会让
"两个 antd 哪个是真的"变成下一个坑);部署断言改为钉四资源 + ConfigProvider + zh-cn。
② 影院下拉加搜索筛选
· 搜索框放在弹出列表**内部**(`.cs-search` 吸顶),打开下拉**即聚焦**;回车选第一项。
· 🔴 `.cs-item[hidden] { display: none; }` **必须显式写** —— `.cs-item` 自己声明了
`display:flex`,会盖过浏览器默认的 `[hidden]{display:none}` ⇒ 过滤"看着完全没生效"
(项目第 11 批选人组件踩过同一个坑)。
· 三个事件都 stopPropagation,否则冒到 document 的"点外面关"⇒ 一点搜索框下拉就没了。
· 「不记录影院」恒保留(清空选择的出口);收起时清空搜索词(否则下次带着旧过滤)。
③ 简介 / 备注各占一行
· 撤销 v38 的 `.mv-pair` 并排(半宽的多行文本框每行都换行,更难读);
CSS 与模板里的并排容器**全部清干净**。
④ 海报 / 观影照片与简介对齐 —— 它们本来就不需要特殊规则
· 用户看到的"更窄/不一样"有两个原因:① 海报那块被误包进 `.mv-grid.mv-pair`
且**里面只有一个孩子** ⇒ grid 两列让它只占**半宽**;② 标签被一条
"宽度自适应、左对齐"的规则覆盖掉了固定 62px 右对齐 + 冒号。
· ⇒ 两块都改成普通 `.mv-row.mv-row-media`,标签用 `.mv-lb`(**继承**简介那套),
内容包 `.mv-media-body`(占满剩余);观影照片标签加 `.no-colon`。
实测:所有控件左缘 **424**、宽度 **624**,与简介**完全一致**。
· 🔴🔴 **通识**:v38 的 `.mv-media`(手填 margin-left:72px)是**被一个取错元素的
探针误导出来的** —— 它按文档序取"第一个非标签子元素",而 `_stage_drop.php` 吐的
`.sr-only` 文件框是 `position:absolute`(**脱离文档流、不参与 flex**),却被算成
"控件",于是得出"1px 隐框把上传框推走 130px"的**假结论**。
⇒ "对齐"类断言必须取直接子元素、且只取真正参与布局的那个;量到反常数字先怀疑探针。
验证(全部真机 + 反向验证):
· _fb_b39_movie.py **39/0**(中文化三条 + 搜索框打开即聚焦/输入即过滤/无匹配空态/
点搜索框不关下拉/回车选中/重开已清词 + 简介备注各占一行且占整行宽 +
海报照片标签字重字号同简介/海报带冒号/照片不带/上传框左缘与宽度同简介);
· _fb_b39_narrow.py **21/0**(宽 1440 / 平板 768 / 窄屏 390:不横向溢出、
标签与控件仍同行、影院下拉不出视口、搜索框可用);
· _fb_b39_falsify.py **14/0**(四处各自证伪成功:拿掉 ConfigProvider→按钮变英文、
藏起搜索框→②红、塞回两列并排→③红、标签退回普通 span→④红;复原回全绿);
· v38 两套按 v39 更新后 **36/0**(新增"已无 mv-pair / 主列 6 行");
v38 的旧窄屏套件(断言"简介备注并排")已作废删除,由 v39 窄屏完全覆盖;
· 全路由 e2e **1503/0**、日期控件真机 **53/0** + 反向 **7/0**、v37 两套、
观影照片两套、b30 三件套、各批真机与 DAV 三套、部署静态断言空跑 —— 全绿;
· 线上四个 antd 资源 HTTP **200 可达**;app.js / app.css / form.php / layout.php
的 SHA256 与本地**逐字节一致**。
⚠️ 顺带修的探针/夹具问题(都不是页面 bug):
· 夹具库重铺后 `cinemas` 是**空的** ⇒ v39 探针**自带播种**(造两家影院),
不再依赖别的套件先跑过(与 v38 的"名册只有 1 人"同类:页面没问题,是夹具没数据);
· 部署断言第一版按裸词 grep `mv-pair`,把注释里那句"已撤销 .mv-pair"当成回归
⇒ 改为只认 `class="...mv-pair..."` 这种**真的类名用法**;
· v39 探针第一版判"简介/备注各占一行"看的是"是不是同一个 DOM 节点"——
那个在**并排**时也是 false(并排仍是两个 .mv-row),连并排都抓不住
(反向验证当场抓到 13/1)⇒ 改为按**垂直位置**判(top 相差 ≥20px)。
|
||
|
|
8661ab6475 |
v38 第 38 批:观影试点 Ant Design 日期控件 + 弹框内联排版(对齐)+ 去提示 + 默认带自己
用户原话(四条):
①「我终于找到了一个我比较认可的日期选择插件
https://ant-design.antgroup.com/components/date-picker-cn 现在观影这个模块进行
测试吧,测试那个有开始、结束时间的。等这个模块测试完没问题了,再更新到其他的模块。」
②「记录一场电影,这个弹框因为标题和下面的输入框都是两行的,所以导致弹框非常长。
能不能排排版,让前面的标题后面加个冒号,后面的输入框直接跟在它后面,这样就变成一行。
包括开始、结束时间、影院、同行的人、简介等等,能把它排成一行的都排成一行,
但是一定要注意对齐的问题,如果不对齐的话可能不好看。」
③「里面的一些提示,比如片名可以从豆瓣自动补充,还有刚新建的影院会自动选中,
观影照片可以放这里且可多选,这些都把它去掉吧,节省一下上下的空间。」
④「另外一个同行的人,这里每个人添加的时候,应该默认带上自己,也就是自己不用选择
就应该已经选上了。」
① Ant Design RangePicker —— **只在观影试点**(用户明确"试完再推广")
· 资源 `assets/vendor/antd/{antd.min.js, dayjs.min.js, reset.css}`(unpkg 下载、随包上线);
layout.php 加 `fb-antd` 清单;`_datetime.php` 加 `$dtPlugin = 'antd'` 开关 +
`data-dt-antd="1"` 标记;app.js 加 `bootAntd()` / `mountAntd()`(按需加载)。
· 🔴 UMD 的硬依赖:antd 从**全局**取 React / ReactDOM / dayjs。缺 React 的报错是
`Cannot read properties of undefined (reading 'createContext')` —— 表现是"控件完全
不出现",不是样式问题。项目本来就有一套 React 18.3.1(Semi 用的),直接复用同一份。
· 🔴 实测出的关键行为(独立探针 `_antd_probe`,15/0):面板里有 **42 个日期格 +
2 个内联时间列(84 项)+ 「确定」按钮** —— 正是之前 Semi 缺的那两样;
且 **`onChange` 只在两端都齐了才触发**,单端改动走 `onCalendarChange` ⇒
回写必须两个都挂,否则"只填了开始时间就提交"会丢值。
· ⚠️ 原生那对下拉**全部保留**(值载体 + 拉不动时的兜底),挂上 antd 后用 clip 视觉收起
(不用 display:none —— 保持可聚焦)。
· 部署断言钉住试点边界:日程/任务**不该**出现 `$dtPlugin = 'antd'`。
② 弹框排版:标签内联 + 控件左缘对齐
· `.mv-row` = 标签(固定 62px、右对齐、**冒号由 CSS ::after 给**):控件(占满剩余)。
· 并排对:简介+备注、海报+观影照片 → `.mv-pair`(grid 1fr 1fr 保证两列等宽);窄屏回单列。
· 🔴 真机量出**两个真对齐缺陷**(不是猜的):
1) 时间行的控件左缘比别的行**偏 8px** —— `.dt-cols` 是 `gap: 10px 18px`(列距 18),
而普通 `.mv-row` 只有 `gap:10px` ⇒ 改 `.mv-row.dt-range.dt-cols { column-gap: 10px }`;
2) 海报/观影照片两行歪了 **130px** —— `_stage_drop.php` 在最前面插的 `.sr-only`
`<input>`(绝对定位 1px)**占掉了一整份 flex:1 1 auto**,把上传框推出去
⇒ 这两块改纵向 `.mv-media`(标签一行、内容从 72px 起,与主列对齐)。
⇒ 现在**所有字段控件左缘都是 444**;A/B 实测**弹框 1036px → 817px(矮 219px)**。
③ 删掉三句冗余提示(豆瓣补充 / 新建影院自动选中 / 观影照片),简介 rows 6→3。
④ 同行的人默认带上自己:新增时预置 `[我]`;编辑以库值为准;`old()` 存在时尊重用户上次勾选。
🔴 顺带修的真 bug:**antd 面板与弹框抢 Esc** —— 两个处理器都挂 document 上,
按一次 Esc 把日期面板和整个弹框一起关掉。已改成"antd 面板开着时弹框不吃 Esc"。
验证(全部真机 + 反向验证):
· _fb_b38_movie_form.py **33/0**(antd 挂上/面板有时间列与确定/值写回隐藏 *_date/
标签同行与左缘对齐/三句提示消失/默认带自己/试点边界);
· _fb_b38_narrow.py **6/0**(宽屏并排、窄屏回单列、无横向溢出);
· _fb_b38_falsify.py **8/0**(排版退回上下两行、antd 只挂 onChange、不预置自己 → 各自必须红);
· e2e **1503/0**、日期控件真机 **53/0** + 反向 **7/0**、v37 两套 **27/0 + 5/0**、
观影照片 **16/0** + **8/0**、b30 三件套、b18 选人 **10/0**、几何交互 **109/0**、
DAV 三套、部署静态断言空跑 —— 全绿;
· 线上 app.js / app.css / antd.min.js 的 SHA256 与本地一致。
⚠️ 顺带修了探针/夹具的三处**测试侧**问题(都不是页面 bug):
· e2e 里 6 条 jsdom 用例(测选人弹框"暂存区机制")前提是"一个都没选",
默认带自己后全假红 ⇒ 在**测试自己的 HTML 副本**上清掉 `data-pp-selected`;
· `_fb_b18_picker` 真机探针同理,改成**点 chip 上的 ×**(组件把已选存在闭包里,
改属性没用 —— 试过两种写法都是白试);
· `_fb_b34_dtrange` 的"标签文案"断言写死了「开始 / 结束」,
而 v38 把观影的标签简化成了「时间」⇒ 改成"有非空标签即可";
另补一条 antd 分支(挂 antd 后原生被裁成 1px,`1px > 0` 会让旧判据**假通过**)。
· 顺带:夹具名册只有 1 人时会误报"无法验证多选",已把实际候选数打出来。
|
||
|
|
d83c94e127 |
v37 第 37 批:日期控件换成「年/月/日 + 时/分」五个原生下拉 + 五处界面重做
用户原话(6 条,一次给全):
①「观影详情页面。观影图片下面有一个块…要加照片请点上方「编辑」…这个块不要,
直接把这个块删除。」
②「编辑页面的时间…选日期的这个控件非常非常不好用。就是如果我把年和月那个地方
拉下来,我选完年和月之后,它并不会回到让我选日的那个界面,它还是不动的…
要么换一个控件,要么在那个地方加一个确定按钮…现在开始和结束时间分在了两行…
能放得下的话就应该放一排…不要让它默认显示在两行上…"散场时间可不填。"这句话去掉。」
③「首页上今天的日程…分了几列,列之间的空隙非常非常的大…日程里面的那两个色块
就是紧挨着的。所以…应该让它们紧挨着。」
④「现在怎么部署的时间这么长?…你修改了哪些页面,你就只测试哪些页面就可以了。」
⑤「日程模块的新建日程…"加上"和"取消"两个按钮在右上角,离得特别远,根本看不到…
每个输入框前面的标题都太小了…都应该加粗、变大。」
⑥「1.新建任务的截止时间,也和观影一致。2.新建任务时竟然没有添加附件…
3.新建任务也弹出一个框,新建完之后刷新看板页面。4.观影新增也弹框输入所有信息。」
② 日期控件:**真根因是"这个控件给不了"**
`<input type="date">` 的日历弹层由**浏览器自己画**,我们碰不到 DOM、也控制不了流程;
Chrome 在"点顶部 `2026年 10月` 切到年月视图"之后,要靠用户**再点一次日期或按 Esc**
才回得到日期视图,各家还不一致 ⇒ 用户遇到的"选完年月就卡住、选不了日"。
修:`_datetime.php` 重写成 **年 / 月 / 日 三个原生 <select> + 时 / 分**,
选完即生效、无需"确定";秒默认去掉(用户"只记录到时和分")。
⚠️ 提交契约**一字未改**:`*_date` 仍是真字段(改 `type=hidden`)+ `*_hour/_minute`。
⚠️ `required` 落在隐藏控件上会让 Chrome 报 "not focusable"(点保存什么都不发生)
⇒ 改用 `data-dt-required` 声明、JS 自查(12 个调用点全部受影响)。
起止两端:`.dt-range.dt-cols` + `flex-wrap` → **宽屏一行、窄屏自动换行**
(不用 JS 量宽度:字体/动画/缩放时量不准,flex-wrap 是浏览器布局的一部分)。
🔴 顺带挖出真 bug:旅程「修正」表单的 `.dt-range` 只有 **22px 宽**
—— 它长在 `.node-ops`(一排小按钮的容器)里,展开的内容只有"修正"两个字那么宽。
修:`.rev-panel{position:absolute}` 纯 CSS 浮出来(不动 DOM,避免牵动节点列表排版)。
③ 首页冲突色块"空隙特别大":与日历页逐字对比出来的
日历页 `left: calc(2px + L%)` / `width: calc(W% - 4px)` → 只留 4px、紧挨;
首页却写死 `calc(46px + L%)` / `calc(W% - 50px)` —— 每块左右各扣 23/25px,
两列时那 46px **没法均分**、全挤进了中间的缝(实测 ≈50px)。
修:`left: calc(46px + L% + 2px)` / `width: calc(W% - 4px)` → 缝恒为 4px。
⑤ 日程弹框"按钮看不到":**两个**根因叠加
① `calendar/index.php` 里**多写了一个 `</div>`** —— `.cal-modal-panel` 被提前闭合,
底栏 `<div class="cal-modal-foot">` 成了 `.cal-modal` 的直接子元素、被挤到面板外;
② `.cal-modal-panel` 是 flex 列,但**它下面的 `<form>` 不是** →
`.cal-modal-body{flex:1 1 auto}` 的上下文只到 form 为止,body 压不下去也不滚。
另有 `.cal-modal-panel` 的 max-height 没算上 `.cal-modal` 自己的 padding(差 4px)。
三处都修;标签只对**弹框里**的 `.field > label` 加粗放大(14px/600/深色)。
⑥ 任务新建:内联卡片 → 弹框;**新增附件**(`attach[]`,服务端零改动能收);
`back=tasks` → 新建完回**看板**刷新。观影新增:列表页内嵌同一份 form.php 弹框。
① 详情页删掉那句提示块。④ 流水线改**增量**:`run_suite_if <标签>` + 从 git 改动
推模块;**一碰共用件就退回全量**(app.js/app.css/layout/index.php/_datetime.php…),
认不出来也全量(保守:少跑一段发坏东西的代价远大于多跑十分钟)。
🔴🔴 本轮最值得记的教训:**DOM 嵌套错误比语法错误难查得多**
在 movies/form.php 与 calendar/index.php 各写坏一次嵌套(多/少 `</div>`、多 `</form>`),
两处**都通过 php -l、页面也打得开**,症状却是"按钮跑到面板外"与
"编辑保存静默失效"(保存键落到 form 外面)。⇒ 模板里 `<?php if ?>` 与 `</div>`
必须**按分支配对**,改完用真机渲染 dump 一次 div 深度;光靠 php -l 发现不了。
验证(全部真机 + 反向验证):
· _fb_b34_dtrange.py **61/0**(四个页面:年/月/日/时/分 都可见可点 +
选完三个下拉**立刻**写回 *_date + 改月重建"日"列 + 起止默认同一行 + 无 JS 报错);
· _fb_b34_dtrange_falsify **7/0**(三处注入各自必须变红);
· _fb_b37_ui.py **27/0**(详情页无提示块 / 首页冲突缝 ≤8px / 弹框按钮在面板内且在
视口内 / 标签加粗 / 任务弹框+附件+回看板 / 观影列表页弹框);
· _fb_b37_falsify **5/0**;
· _fb_b35_movie_media.py **16/0**、_fb_geom_interact **109/0**、e2e **1503/0**、
b30_ui2 16/0(含首页冲突分列)、_b30_import 26/0、b18/b21/b23/…全绿、
DAV 三套全绿、部署静态断言空跑通过;
· 线上 app.js / app.css / movies/form.php / calendar/index.php 的 SHA256 与本地一致。
· 顺带修掉 _b30_import 的一个**时序竞争**:它用外部上游(联想词服务)的二次探测
当对照,浏览器取词在前、探测在后,上游抖一下会误报成"回归"(已改成两次确认)。
|
||
|
|
2fda455a59 |
v36 第 36 批:时间控件改「日期 + 时/分下拉」(修掉 v35 真没生效的可见性 bug)+ 详情页去掉补照片
用户原话:
①「观影模块,开始时间和结束时间是空白的,无法选择,可以调整为,前面是日期,
后面是时和分,只记录到时和分就行了。时和分,是下拉的。」
②「观影详情页面,不需要设置补充照片的功能,都放在编辑页面就行了。」
① 时间控件:**v35 的修复根本没生效**(我的锅),而且探针连绿两轮没抓到
真因:upgradeRange 里只给范围**组**加了 `.is-native-range`,
**没给两个端点 `.dt-input` 加 `.is-native`** ⇒ CSS
`html.js .dt-input:not(.is-semi) .dt-native { visibility: hidden }`(特异性 0,3,0)
照样命中 ⇒ 控件 **visibility:hidden**:**占着 48px 高度、完全不可见、点不到**
(用户看到的"一片空白")。
🔴 探针为什么没抓到:它用 `offsetParent !== null` 判"可见" ——
而 **visibility:hidden 的元素 offsetParent 仍然非 null**,于是连绿两轮。
⇒ 判可见改成 computed visibility + display + getBoundingClientRect 有宽高
(_fb_b34_dtrange.py 与 _fb_geom_interact.py 都已改,后者直接用 is_visible())。
修法(比 v35 更笨但更稳):**不造新控件、不加 class 猜 CSS**,直接
① 端点加 `.is-native`(压掉藏原生那条规则);
② 删掉"秒"那一档(连它前面的冒号)—— 用户"只记录到时和分";
③ **inline 强制 display/visibility**(双保险;v35 就是被历史 CSS 规则翻掉的)。
CSS 侧关键几条加 `!important`,并删掉 v35 遗留的 `.dt-merged` / `.dt-time-input`
规则(免得后人误以为还有"收起来的下拉")。
形态 = 「日期框 + 时下拉 + 分下拉」,提交契约仍是 *_date/_hour/_minute
(秒栏不再有,服务端 read_dt_input 对缺失 *_second 按 00)。
② 详情页移除"补一张照片"表单(form.photo-add),留一句"要加照片点上方编辑"。
⚠️ 服务端 `movies/photo_add` 路由**保留**(老书签/直接 POST 仍可用),只是不再有入口。
验证(全部真机 + 反向验证):
· _fb_b34_dtrange.py **37/0**:四个表单页断言「日期框可见 + **时**下拉可见 +
**分**下拉可见 + **没有"秒"** + 只改时分也能提交」;
· _fb_b34_dtrange_falsify.py **5/0**:注入"去掉 .is-native"(= v35 的真错)
→ **12 条断言变红** ⇒ 证明新判据真的能抓到那个 bug;
⚠️ 而"只撤 CSS !important"或"只撤 inline 强制"都 0 条变红(被别的兜底救回来)——
做证伪必须挑"真正决定行为的那一层";
· _fb_b35_movie_media.py **16/0**(⑥ 改为"详情页没有补照片入口");
· e2e **1503/0**(详情页入口断言反过来)、几何交互 **96/0**(+4 条时分下拉);
· 各批真机/反向全绿、部署静态断言空跑通过;
· 线上 app.js / show.php 的 SHA256 与本地一致(v36 已上线)。
|
||
|
|
5c81f1991f |
v35 第 35 批:日期控件改用原生(Semi 时间面板不可用)+ 观影附件上传真修好 + 票根/合影归并成「观影照片」
用户原话(三段):
①「选择日期那个地方给,非常不好用。如果我不选日期,只选时间,框里面仍然是空白的,
也没有确定按钮,这个需要重新调整一下。」
②「现在上传附件,也就是在观影模块里面上传附件,怎么还是有问题?上传的附件现在
都没有保存,有时候我上传两个附件,它只保存一张。」
③「除了海报之外,票根、现场照片等等都不分类别,都只分一个类,放在一块。…
只有海报是单独的,因为它是从豆瓣里面下载回来的。其他的都放在一个模块就行,
然后所有照片都上传到一起,直接写"观影照片"就可以了。」
① 日期控件:**Semi 的时间面板根本不可用**(这条最费时间)
实测(独立最小复现页 + 真机):Semi 的 dateTimeRange 面板**只给日期网格**,
时间藏在 `.semi-datepicker-switch-time` 那个小时钟按钮后面;**点了它切到时间面板,
面板仍然是空的**(timeLists=0、无选项、无 footer、无"确定")→ 根本无法"只选时间"。
单点 dateTime 同样。⇒ 与用户描述完全一致。
修:弃用 Semi 的时间选择,改用**原生** date + time(step=1)
(24 小时制、精确到秒、点了就选、不选日期也有值、**不需要**确认按钮)。
原来的 时/分/秒 三个下拉**仍留在 DOM**(POST 契约 *_date/_hour/_minute/_second
一字不改),只是加 .dt-merged 视觉收起。
app.js 的 upgradeRange()/upgrade() 都不再挂 Semi;CSS 加 .is-native-range 系列
把 Semi 时代的"藏原生 / 两端让位"规则覆盖回来。
② 观影附件上传丢文件:**两条路径都断了**
· 暂存路径(拖放):上传框走"拖进来即刻上传到暂存区",令牌进 stage_token[];
而 movies/create|update 与 movies/photo_add **只读 $_FILES['photo']**
⇒ stage_token[] **从来没人认领** ⇒ 拖进去的一个都没存。
· Movie::update() **完全不写 photos**(那句"photos 由单独的删除路由维护"的注释下
根本没写),而路由明明算好了 $d['photos'] 传进来 ⇒ 编辑保存丢照片。
修:路由新增 stage_token[] 认领循环(与 $_FILES 路径并存);photo_add 加兜底;
Movie::update 补上 savePhotos。
③ 归并:表单去掉独立"票根"上传框,"现场合影"并入**一个**「观影照片」块;
详情页「影像记录」→「观影照片」、去掉票根/照片分类标签。
数据层本来就是平的(event_photos 只有 key/position);
⚠️ 旧数据 movie_details.ticket_key **仍要显示**(历史票根不能丢)+ 服务端仍接收
$_FILES['ticket'](向后兼容,直接 POST 仍可存)。
验证(全部真机跑,不是"看代码觉得对"):
· _fb_b34_dtrange.py 33/0(四个表单页:组可见 + 两端日期框可见 + 两端时间框
可见且 step=1 + 三个原生下拉仍在 DOM + 只选时间也能写回 *_hour/_minute/_second);
· _fb_b35_movie_media.py 14/0(点击上传 2 张→库里 2 行;真 HTTP 打 upload_stage.php
拿 stoken→随表单提交→落库;编辑补传→3 行;详情页补传→4 行;归并断言);
· 反向验证:_fb_b34_dtrange_falsify 4/0、_fb_b35_media_falsify 8/0;
· e2e 1503/0(两条断言随 v35 语义更新)、几何交互 92/0、各批真机/反向全绿、
部署脚本静态断言空跑通过;线上 app.js / form.php 的 SHA256 与本地一致。
顺带修掉的工具链脆弱(本轮暴露):
· push.sh 开头加"确保本地测试库在跑"的守卫(长时间跑流水线时 mysqld 会被环境
回收 → e2e/真机探针全崩,看着像代码坏了);
· 探针别用固定字符窗口找函数体(注释一长就假红)、断言别写死字段名前缀
(改期表单是 rev_ends_at_*)、set_input_files 的 id 要照 _stage_drop 的净化规则
(photo[] → sd-photo)、提交按钮要精确定位(编辑页有多个 submit)。
|
||
|
|
9c70dbe0ff |
v34 第 34 批:观影「开始/结束时间没了」真根因修复
用户原话:
观影模块儿的开始和结束时间都给我搞没了。赶紧修复一下。
真根因(真机实测):
app.js 的 upgradeRange() 挂上 Semi 范围控件后,会把两端**各自的包装 .field**
用 display:none 藏掉(只留范围控件)。但观影 / 旅程表单的结构是
**两端共用一个 .field.dt-range**:
<div class="field full dt-range">
<label>开始 / 结束时间(精确到秒)</label>
<div class="dt-input">开始
<div class="dt-input">结束
</div>
此时 a.closest('.field') 与 b.closest('.field') **都取到 .dt-range 本身**
=> display:none 把**整组(含刚挂好的范围控件、连标签)**藏掉
=> 用户看到「开始/结束时间整个没了」,点不到控件,提交时四个原生字段全空
=> 库里 starts_at/ends_at 为 NULL。
真机证据:{groupDisp:'none', groupVisible:false, groupH:0, rangeVisible:false}
线上数据证据:今早 08:43 新建的「神探之痕迹」两条时间都是 NULL(就在反馈前几分钟)。
服务端**完全正常**:直接 POST movies/create(带 starts_at_*/ends_at_* 四字段)
-> 库里正确落 2026-10-05 19:30:00 / 21:45:00 => 问题纯在前端渲染。
⚠️ 日程页为什么没事:它两端各在自己的 .field.ev-dt 里(两个不同包装),
分别隐藏是对的。所以这个 bug 只在"两端共用包装"的三处模板出现:
movies/form.php、trips/node_form.php、trips/_revise_form.php。
修法(assets/js/app.js):
隐藏前先判"包装是不是就是范围组本身":
var wrapIsGroup = (hideA === group || hideB === group || hideA === hideB);
if (wrapIsGroup) { 藏里层 .dt-input、组本身保持可见 }
else { 原行为:两端各藏自己的包装 }
(CSS 已有 .dt-range.is-semi-range > .dt-input { display:none },与 JS 修复不冲突。)
验证:
· 新增真机探针 _fb_b34_dtrange.py:四个用时间范围的表单页
(观影新增 / 观影编辑 / 旅程节点 / 旅程改期)都断言
「组可见 + 范围控件可见 + 标签在 + 原生控件已让位 + 无 JS 报错」21/0。
· 反向验证 _fb_b34_dtrange_falsify.py:改回无条件隐藏 -> 断言必须红 4/0。
· 线上 app.js SHA256 与本地一致(含 wrapIsGroup 修复)。
· 全套:e2e 1503/0、几何 428、各批真机/反向全绿、部署静态断言空跑通过。
顺带修掉的两处探针脆弱(本轮暴露):
· _b32_verify.py 的"真机点共享"原来是一串固定 wait_for_timeout —— 流水线里
机子忙时踩空 -> 证伪① 假失败(单独跑却 5/5 全过)。改成 wait_for_function
轮询每一步就绪 + 提交后轮询数据库落库。改后证伪 5/0。
· _b32_falsify.py:加 FB_FALSIFY_ONLY(只跑指定用例,省 80% 排查时间)+
前置清 v32 资源残留 + 每个用例的探针完整输出落盘;证伪脚本夹具起点确定化。
|
||
|
|
df6fe894d0 |
v32 第 32 批(4 条):资源共享弹框真落库 / 消息通知覆盖全部共享入口 / 浏览器通知(站内消息+日程到期提醒) / 首页任务随看板色
用户原话: ①「资源共享功能有问题,弹框中,点击共享给某个人后,弹框消失,但是并没有共享成功。」 ②「消息通知,在所有引用自己的地方,都通知,包括别人分享了日历本给自己, 分享了笔记给自己,分享了资源给自己等等。」 ③「当有消息通知时,还有日历中日程到期提醒时,比如一个日程,设置了10点开始, 提前十分钟提醒,则需要在 9:50 时浏览器中发提醒通知。」 ④「首页,近期任务,任务的文字和前面的圆点,颜色并没有跟看板颜色一致,这个需要修正。」 四条真根因: ① v28 把资源编辑改成三页签后,每个页签各有自己的 form 与 <input name=id>, 而设 id 的代码是 panel.querySelector() → 只命中文档序第一个(info 的 update), ⇒ share/att 表单的 id 恒空 ⇒ 提交 id=0 ⇒ Resource::find(familyId,0,me) 返回 null ⇒ 静默不共享。修:表单打 data-res-idform 标记 + querySelectorAll 逐个填。 ② v30 只在 saveAttendees 后调 syncForTarget,6 个共享入口一个都没接。 新增 Notification::syncShares(家庭,类型,宿主id,动作人) —— 照共享表现状重建差额 (写表后调用;漏调只会没通知,不会状态不一致)。SHARE_KINDS 抽常量。 ③ 完全没有浏览器通知。新增 Calendar::remindersBetween()(窗口留 -2 分钟防后台节流) + ?r=notifications/poll JSON 路由 + assets/js/notify.js(轮询 + Notification API + id@at 去重)+ 顶栏铃铛(权限只能用户点一下才要,自动申请会被 Chrome 静默拒绝)。 ④ v30 把用户那句和主题颜色一样理解成了**任务模块主题色**,用户要的是**看板色**。 新增 Task::boardColorById(家庭,看板id)(没设色按看板名派生稳定色),home.php 两处改 $tkColor。 验证:诊断探针 18/0、真机 25/0(真点共享→库里真的多一行;三类共享真发通知; 9:50 窗口真算出 10:00 那条;首页每条任务色 == 其看板色)、反向 5/0、 e2e 1502/0、升级 153/0、几何 428+680+92、城市 32/0。 部署脚本静态断言段本地空跑通过;线上站点自检 19 条全过,notifications/poll 未登录 302。 |
||
|
|
2a10df2557 |
v30 后半(5 条):导入框选中反馈+单选 / 首页日程改色块 / 近期任务主题色 / 时间冲突错开 / 通知消息模块
用户 2026-10-08 第二批 5 条。 1. 导入日程选中文件后没有任何反应 + 不允许多选 `_stage_drop.php` 现在有两种模式:暂存型(附件,拖进来即刻上传)与导入型 (`$sdStage=false`,写回 input.files、随表单提交、PHP 读完即弃)。 暂存型框靠 queueDrop 画缩略图,而**导入型框选中文件后什么都不显示** —— 框里那句「点击或拖动进行上传」从头到尾不变,用户看不出"到底选上没有"。 改:app.js 新增 bindFileEcho(),只给**非暂存框**补 change 监听, 选中后框里的话换成文件名 + 图标变 ✓ + `.upbox.is-picked` 稳定态; 取消选择则还原原始文案(不是空字符串)。单选由 `$sdMultiple=false` 保证。 2. 首页「今天的日程」用色块表示,和日程保持一致 原来那套是"白底卡片 + 左边一条 3px 色线",色只占一条线,看不出是哪个日历本。 改:与日历页 `.tg-block` 同款实心色块(底色=日历本色,白字,圆角,无阴影), left/width/top/height 全部由内联给出(冲突分列 + 真实时长)。 ⚠️ `.ag-ev` 的 CSS 里去掉 `right` —— 绝对定位下 left+right+width 三者同时存在是 过约束,内联 width 会失效(冲突就不会错开了)。 3. 首页「近期任务」名称与圆点走任务模块主题色 原来用 `var(--brand)` —— 那是**当前页面**(首页)的主题色,不是任务模块的; 用户改过模块配色时两者不一致(首页砖红、任务别的色),正是他看到的"不一致"。 改:两处都走 `--tk-c`(= Module::color('tasks')),与右上角「全部任务」按钮同源。 逾期仍用红色(状态语义,不能被主题色盖掉)。 4. 日程时间冲突错开(算法借鉴 SyncCalendar) 用户指定去读 SyncCalendar 的算法。落地自 `entry/src/main/ets/common/TimelineUtil.ets` 的 build()/groupsOf(): ① 划分冲突组(与组内最晚结束仍重叠就并入);② 组内贪心分列(区间图着色), 列数 = **每组各自的最大同时重叠数**(sync 原注释点名的反例:传递性重叠但 B/C 不重叠 → 只该 2 列)。落成 `App\Calendar::conflictLanes()`(PHP,判据一字未改)。 ⚠️ 两处比原算法更细,都有单测钉住: · 列数按**组**算 —— 否则一个孤立日程会被别处的三方冲突连累成 1/3 宽; · "缺结束时间"不能一律按 30 分钟参与冲突(会与后面所有日程假冲突), 改为优先拿**同列下一条的起点**当它的视觉结束 → 因此必须**先分列再算高度**。 首页与日历页共用同一套(用户第 2 条要求"和日程保持一致")。 5. 通知消息模块(Schema v30 → v31) 不在导航显示,入口在头像下拉里、**排在个人设置之前**,名字「通知消息」; 有人在观影/旅程加我时发通知;通知出现在首页、**往年今天之前**。 新增:app/Notification.php + notifications 表(唯一键 (person_id,target_type,target_id) —— 参与人是整组替换,没有它改三次标题就收三条)+ ?r=notifications{,/del,/clear} + templates/notifications/index.php + 头像下拉未读角标 + app/helpers.php 的 fb_time_ago()。 ⚠️ 三条设计判断:不进 Module::LIST(进去就会被顶栏渲染、被"模块显示与排序"当开关); 只在 person_id !== actor_id 时发(不给自己发,"通知"≠流水账); 移出参与人**撤销未读**、**保留已读**(未读是没送达的提醒,已读是看过的历史)。 验证(全部 0 失败):e2e 1478/0(+14 条新断言)、升级 153/0、vCard 40 项、城市 32/0、 几何 428/0 + 移动 680/0 + 交互 92/0、b24/b27/b28/b29 真机全绿; 冲突分列算法单测 12/0(含 SyncCalendar 点名的反例)、 新探针 _b30_ui2 16/0 + _b30_notify 16/0 + _b30_import 26/0、 反向验证 18/0(五条各自改回旧行为 → 断言全红)、 部署脚本新增 9 条守卫 + 守卫证伪 11/0。线上 11 文件 SHA256 一致,Schema v31 已升级。 |
||
|
|
91c55038c9 |
v30 第 30 批(3 条):ICS 导入读完即弃 / 导航联想词根因修复 / 导航设置改纯图标
三条都是「静态检查与 e2e 恒绿、只有真机才现形」的,每条先写真机诊断探针 拿到真根因再改,并逐条反向验证(改回旧行为必须被抓住)。 1. 日程 ICS 导入:文件已上传仍提示「请选择一个文本」 真根因:_stage_drop.php 是"拖进来即刻上传到暂存区"的唯一形态—— 拖放时文件走 upload_stage.php 落到 uploads/_staging/,而 <input>.files 始终是空的;两个导入框(ics/通讯录)都带 required → 浏览器原生校验把提交 拦下,弹它自己的文案。就算绕过校验,路由只读 $_FILES['ics']、从不看 stage_token[],暂存文件永远没人认领。 改:片段新增 $sdStage(默认 true)。导入型传 false → 框上不带 data-drop-stage → app.js 走"写回 input.files、随表单提交"的老路,文件走 PHP 临时文件,路由读完即弃;不落存储、不建附件行、不进 OSS。 同时把 app.js 找框的选择器从 [data-drop-stage][data-sd-for=…] 放宽成 [data-sd-for=…]——否则这些框点不开也接不住拖动。 2. 导航联想词"还是不好用" 真根因:nav_show.php 给出的 suggest 地址已带 ?p=<短语>,而 nav-show.js 又 硬拼 '?slug=…' → 实际是 …?p=abc?slug=bing&q=…;PHP 认为第一个 ? 之后整串 都是查询值 → p 匹配不上短语 → 403 → 下拉永远空。服务端一直是对的(直连 200 + 10 条词),e2e 探针自己拼对了参数,所以 HTTP 层恒绿、只有真页面能抓。 改:nav-show.js 新增 withQuery(),按 base 里有没有 ? 决定分隔符; nav_suggest.php 加短语容错(切掉第一个 ?/& 之后的内容,救缓存里的旧 JS)。 3. 导航设置按钮改纯图标 ☰ v29 只改成了下拉菜单,summary 仍是 btn-ghost btn-sm + 文字「设置」+ 一个 向下箭头。改:换成与其他七个模块逐字节相同的 btn-icon + 三条横线。 验证:e2e 1464/0(新增 2 条)、升级 153/0、vCard 40 项、城市 32/0、 桌面几何全过、移动 680/0、交互 92/0、窄屏 30/0、b24_nav 41/0、b24_ui 26/0、 b28 39/0、b29 38/0、b27 34/0;新真机探针 _b30 22/0、反向验证 11/0; 部署静态断言新增 8 条守卫并逐条证伪通过(本轮抓到 2 条恒真守卫 + 1 处 _code_only 的 locale 差异)。线上 7 个文件 SHA256 与本地一致。 |
||
|
|
37ba86321b |
v29 第 29 批(9 条):日程定位/复选框提示/名册错乱三处真因修复 + 读书落盘 + 导航四设置 + 笔记阅读态与共享
前 4 条是"上批改了但用户仍报"的,这次先拿真机证据再动手,三处真因如下:
1. 首页没定位到红线 —— 真根因:定位脚本的 PHP 条件是
`if (!$todayEvents || empty($timed))`,**今天只有全天日程时整段脚本不渲染**
(实测 hasScript=False、scrollTop=0),而 .ag-grid(0–24h) 与红线都在。
有分时段日程时定位其实完全正确(偏差 0px)。已改成 `if (!$todayEvents)`。
2. 任务复选框提示还在 —— 真根因:`_task_check.php` 里那句无障碍文字
`<span class="visually-hidden">` 用的类名**在 app.css 里从来没有定义过**
(项目一直只用 .sr-only)→ 没有规则 = 普通 span = 直接可见。
已把两个类名合并到同一条规则(clip 隐藏,仍可读屏)。
3. 名册完全错乱 —— 真根因:v29 重写平铺列表时**漏了一个 `</div>`**(闭合 .person),
浏览器把下一张卡嵌进上一张里,实测 .person-main 只剩 24~151px(本该 ~800px)。
`php -l` 与全部静态断言都抓不到,只有真机量 DOM 才现形。已补上闭合。
另把 v28 引入的多列 grid 改回单列(它与 content-visibility 冲突)+窄屏动作区折行。
4. 读书慢 —— 按用户给的思路「做一个同步,然后落盘」:
新增 weread_snapshots 表 + App\WeReadCache(按人+数据集存 JSON);
渲染路径改走 notebooksCached()/readStatsCached()(永不联网);
同步从"渲染之前"挪到"**响应之后**"(queueBackgroundSync + fastcgi_finish_request)。
5. 导航设置改成右上角**下拉菜单 + 四视图**(addr/general/board/engine),
表单带 back 域 → 保存后回原来那一项。
🔴 顺带修掉一个真 bug:`if (!$inSettings && $canWrite)` 的 endif 漂到了文件末尾,
把 board/engine 两个视图也包进了"非设置态" → 打开时标题对、**内容是空的**。
6. 笔记列表缩略图:cover → contain + 固定高度(按比例完整显示,不裁切)。
7. 笔记段落编辑器:加 Markdown 工具条 + 斜线命令(行首打 /co → 回车补全代码块),
两者共用同一份命令表。
8. 笔记点进来默认**阅读态**(图文混排、附件在最下方、图片四档宽度、点「编辑」才可改);
清单块并入 Markdown —— 但**历史 list 块转写成 `- 项` 文本,老数据不丢**。
9. 笔记共享改成**逐人 + 权限**(note_shares,与 resource_shares 同构);
可见性收敛成全站唯一的 Note::VIS_COND;迁移用一次性哨兵。
|
||
|
|
6852b4ee2c |
v28 第 28 批(12 条):名册分组改标签 / 资源合并为两按钮 / 首页定位与主题色 / 笔记富文本双模式
其中 4 条是**上批遗漏的真 bug**(1/4/8/9),根因已逐条定位:
1. 首页日程没定位到红线 —— 真根因:刻度区间**不含当前时刻** → $showNow=false
→ 红线**整个不渲染**,而定位脚本用同一个越界比例算目标,被 clamp 到底部。
修:把当前小时并进区间端点(现在必然落在区间内)。
2. 「看整月」→「更多日程」+ 日程主题色;3. 「手头在做」→「近期任务」、
「任务看板」→「全部任务」+ 任务主题色(注入 --hd-c,不再写死白底)。
4. 任务复选框提示还在 —— 真根因:**JS 在点击后**给状态徽标挂 title
(`badge.setAttribute('title',…)`),上批只去了静态渲染,静态断言永远抓不到。
5. 名册「分组」→「标签」(用户:一个人可能既是小学同学又是初中同学,没法分组):
新增 person_tags(每人一份、一人多标签),名册改**平铺 + 顶部标签筛选**;
🔴 迁移用**一次性哨兵**(第一版用 NOT EXISTS,实测被"用户清空标签"绕过 → 旧分组名被灌回)。
6. 名册卡片按钮加语义底色。
7. 资源四按钮 → **编辑 / 删除**两颗:三块内容进一个带页签的弹框(信息/附件/共享)。
8. 财务多选数字不变 —— 真根因:前端发 `kinds=a,b`(逗号串),服务端 `(array)` 收成
单元素 → 求交为空 → **静默回落整本账**。修:两种形状都收。
9. 微信读书偶发很慢 —— 真根因:syncNow 扫 60 本 × 每本 1~3 次网关调用(单次超时 20s),
全同步阻塞在列表页请求里。修:总时长预算 + 单次超时 8s + 自动/手动分批。
10. 新增全局可见的联想接口库(6 个实测可用来源)+ 三种新解析类型。
11. 导航右上角「设置」:默认进"添加网址 + 全部网址",四类设置收进 ?set=1 视图。
12. 笔记:段落/清单支持 Markdown(自研渲染器,**不允许原始 HTML**),
实时预览走服务端(前后端只一份规则);底部附件上传;清单多级;
列表改**定尺寸卡片网格**(溢出显示摘要、图片缩放下方);按用户要求**去掉标签侧栏**。
|
||
|
|
404b1afee0 |
v27 个人笔记模块:分块(段落/图片/清单)+ 单层多标签 + 默认私有可共享
参考 flomo(时间流卡片 + 标签即筛)、Obsidian(图文混排的直觉)、
思源(「块」的粒度),但刻意只取轻的那一层:
- 分块只做 text / image / list 三种,可增删、可上下移;不做块引用 / 块 ID 双链
- 标签单层、多对多,只存字符串(**不建标签表**)——改名/合并这类重型能力一律不做
- 不做富文本工具栏、不做 Markdown 预览、不做拖拽排序(只有 ↑↓ 按钮)
- 可见性默认**私有**,单条可勾「共享给全家」;共享出去只给别人看,不给别人改
落地:
- 新增 app/Note.php(块清洗、标签归一、可见性、认领草稿附件)
- 新增 3 张表 notes / note_blocks / note_tags;Schema VERSION 26→27
- 🔴 attachments.target_type 是 ENUM:加 'note' 必须同时改建表串 +
新增 widenAttachTargetEnum()(老库不做这一步,传笔记图片会被严格模式拒掉)
- 路由 5 条:notes / notes/form / notes/save / notes/destroy / notes/upload
- 模板 templates/notes/index.php(列表)+ form.php(分块编辑)
- JS assets/js/note_editor.js(按需加载,同 person_picker 那套)
- 编辑器状态放 DOM 而非 JS 数组:移动块时用户已输入的内容一字不丢
|
||
|
|
c8839fa728 |
v26 第 26 批(5 条):财务多选按集合取图 / 首页全天置顶与红线定位 / 任务复选框去提示与看板颜色 / 导航搜索引擎切换与站点复用 / 详情页去复选框
- 财务:勾选多个标签时上面那张「总金额」图按**选中集合**统计(新增 ledger/total_chart 路由 + 单一容器 data-ck-total-host,按集合现取并缓存; Ledger::timeline 第二参数放宽为 string|array) - 首页日程:全天日程移出滚动容器固定置顶;进场把当前时刻红线居中 (scrollTop 计算,不用 scrollIntoView),触发条件不绑 $showNow - 任务:复选框去除 title 提示;看板可设颜色(Task::boardColor/updateBoard), 首页任务行用该颜色显示所属看板 - 导航:展示页补回 id=engine-tabs(切换搜索引擎无效的根因);补内置引擎; 新增站点输 URL 可复用他人已加过的站点(sharedByUrl/copyIconFile/adoptSite) - 任务详情页页头去掉复选框(已有「标记完成」按钮) |
||
|
|
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() 不再报错。
|
||
|
|
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`
|
||
|
|
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 + 窄屏)
|