5 Commits
Author SHA1 Message Date
laoyang fc81347e8d fix(notes): 调过宽度的图片保存后变孤儿(#18)
用户报「笔记功能,上传图片失败」。实际不是上传失败,是上传成功后
保存那一步把图片弄丢了。

根因:图片块 content 有两种形态
  - `photos/xxx.jpg`      —— 宽度 100% 时,前端不写后缀
  - `photos/xxx.jpg|50`   —— 宽度调到 20~99% 时,serialize() 加上宽度

但 Note::claimUploads() 与 Note::files() 直接拿 content 去匹配
attachments.key(`WHERE key IN (...)` / `isset($used[$key])`),
带后缀的那个**永远匹配不到**,于是:

  1. 草稿附件(target_id=0)认领失败 ⇒ 图片变孤儿,笔记里图没了
  2. files() 排除不掉已用作图片块的附件 ⇒ 同一张图既当图片块、
     又在底部附件区重复出现

默认 100% 不写后缀,所以表现为"有时好有时坏",
极易被误判成上传环节的问题。

修法:新增 Note::imageKey() 统一剥 `|宽度` 后缀,
claimUploads() 与 files() 都先过它。

刻意修在**服务端**、而不是"前端别写后缀":后者会把宽度功能废掉
—— 渲染时要按 content 里的宽度出图(见 templates/notes/form.php
的 $imgParts 与 templates/notes/index.php:98,那两处本来就剥了,
所以只有服务端这两处漏了,属于不对称实现)。

updateBlock() 走同一条 claimUploads(),自动被覆盖。

验证(本地 PHP 8.5 + MariaDB,真实 UI 操作,非手搓 DOM):
- 主判据 _tooling/_b35_width_falsify.py:修复前 100%→1712 成功 /
  50%→target_id=0;修复后两条路径都认领成功,7 通过 0 失败,
  且 content 仍带 `|50`(宽度功能没被牺牲)
- 附件不重复 _tooling/_b35_dup_attach.py:阅读态底部附件 0 条、
  图片块 1 张、img 61px vs figure 990px(宽度真的生效),5 通过 0 失败
- 判据有牙齿:把 claimUploads() 注入回旧形态后立刻 2 条 FAIL,
  恢复后回到 7 通过 0 失败

工单 #18
2026-10-28 16:03:36 +08:00
laoyang 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% 在别的批次数据上假红)。
2026-10-15 23:04:25 +08:00
laoyang 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;迁移用一次性哨兵。
2026-10-08 10:54:43 +08:00
laoyang 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**),
    实时预览走服务端(前后端只一份规则);底部附件上传;清单多级;
    列表改**定尺寸卡片网格**(溢出显示摘要、图片缩放下方);按用户要求**去掉标签侧栏**。
2026-10-08 02:20:19 +08:00
laoyang 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 数组:移动块时用户已输入的内容一字不丢
2026-10-07 18:22:55 +08:00