Files
rond-with-you/.workbuddy/memory/MEMORY.md
T
tamakyi ab7ccd22ce 前端改造:设计令牌 + 卡片层级 + 图表可读性 + 类型色收口 + 窄屏导航
先出方案(frontend-plan-2026-10-09.md)再动手,四批按顺序落地。方法上先存基线、再用同一份
探针在改动前后逐值 diff —— 这批里有 6 处问题只有「量」才看得见,看截图全是正常的。

A 设计令牌(app.css 为主)
- 字阶收敛到 6 档 --fs-xs/sm/md/lg/xl/2xl = 11/13/15/21/26/34px。原来 app.css 有 19 种字号、
  模板里另有 54 处内联字号,光 12~15px 之间就挤了 4 档 —— 层级挤不出来,只能靠加粗硬顶。
- 两个「字外档」不是漏的:--fs-control: 16px(窄屏表单控件必须 ≥16px,否则 iOS 聚焦放大整页,
  属功能约束);--glyph-*(✕/箭头/空态图标按控件尺寸定,硬塞进字阶只会把按钮撑坏)。
- 字重收敛到 400/500/600(删掉 650 与 700)。
- 圆角 4 档 --r-*、阴影 3 档 --sh-1/2/3(暗色整套覆盖)、间距 --sp-1..5、
  卡片内边距单列 --card-pad(与折叠卡片标题行的负 margin 绑在一起,改一个即改两处)。
- 门禁 TestNoBareFontSizePx:静态扫 app.css 与全部模板,出现 font-size: <数字>px 即红。

B 卡片分主次
- .card--hero / .card(默认)/ .card--quiet 三档。quiet 走「页面底色 + 细边 + 无阴影」——
  第一版拿 --bg-2 当底色,实测它与页面底 #f6f7f8 只差 4/255,看不出边界、像破了个洞。
- 首屏 KPI 从「4×2 等重白盒」改成 .kpi-lead(主数 + 一行上下文)+ 5 个常规指标,8 个数字一个没少。

C 图表说人话
- 月度分布:柱底加基线;峰值柱不透明拉满、柱顶标次数(.pv),副标题里不再重复写「最高 N 次」。
- 一天中的分布:把「0/6/12/18 四个数字」换成 6 小时一段的标尺(深夜/上午/下午/晚上),
  用同一套 24 列网格,段边界与柱子严格对齐。
- 首页月度图同样加峰值锚点(给 HomeData 补 MaxMonthly,与统计页同口径 maxOf)。
- 空态没动:查下来已经统一(8 页共用 emptystate、局部走 .empty)。

D1 类型色语义化(结论与计划不同,按实测改了)
- 先算对比度:白底上 yellow 1.87 / mint 2.12 / orange 2.20 / green 2.22 / cyan 2.54 / teal 2.57,
  6 个色连图形对象 3:1 都到不了;暗底只有 indigo 2.90 不够。
- 所以**没有**把类型色用到 chips / 表格类型列 / KPI 强调 —— 那几处要的是文字色,只会更难读。
  这也解释了全站为什么一直只用 8px 小圆点做身份标记。浅色那 6 个色一个没动:
  它们是手机端与地图上的同一套身份色,改暗会破坏对应关系。
- 实际做的:色值收口成 --cat-* / --cat-*-soft(26 个变量);模板 colorof → csscolor / csssoft(19 处);
  暗色只提亮 indigo(2.90 → 3.55:1);colorOf 成了死代码,删掉。
- ⚠️ 返回值必须是 template.CSS:html/template 的 CSS 过滤器不认识 var(),普通 string 会被静默
  换成 ZgotmplZ —— 页面上所有圆点当场变透明,而且不报错、模板测试照过。挂了它等于关掉那道过滤,
  所以数据侧色值先过严格校验(只有 # + 6 位 hex 放行),否则 #"><img… 能逃出 style 属性。
  守卫 TestCSSColorSurvivesTemplateSanitizer 把两个方向都钉住(做过变异验证)。

D2 窄屏导航
- 改前 ≤620px 靠横向滚动兜底:390 溢 63px、320 溢 133px,「统计」被切成半个字紧贴后面的按钮,
  渐隐提示实际看不出来。现在链接本体抽成 {{define "navitems"}} 渲染两遍:桌面 .nav-links 平铺、
  窄屏 .nav-menu(原生 <details>,不靠 JS 也能开合);JS 只补点外面 / Esc / 点链接后收起。
- ⚠️ 顶栏 --z-nav 抬到 1200:菜单是 .nav 的子元素、自己抬不动层叠上下文,不抬就会被地图页的
  --z-map-overlay(1100) 盖住。已在 /map 上用 elementFromPoint 实测。
- 守卫 TestNarrowNavCollapsesToMenu 钉「CSS 两半 + 模板两个类 + 渲染两遍」的成对关系。

途中发现并一并修掉的(都只有量 DOM 才看得见)
- Leaflet 的 .leaflet-container 字号是 0.75rem、跟着根字号漂:根 14 → 13px 后地图署名/比例尺
  掉到 9.8px(低于本站 11px 下限)、弹窗正文也跟着缩。已按自己的字阶钉死。
- h1..h4 原来只设了字重与 margin、没设字号,靠浏览器默认的 2em/1.5em/1.17em —— 同样跟着根字号漂:
  /stats 那两个 h2 从 21px 变成 19.5px,比同页卡片标题大一圈。已显式给出字号。
- <b>/<strong> 浏览器默认 700,比全站最高层级(标题 600)还重,已钉成 600。

回归
- ./scripts/verify.sh:PASS=348 FAIL=0 SKIP=0
- internal/fog 加 -race:PASS=22 FAIL=0 SKIP=0
- JS 单测 34/34
- 移动端 320 / 390 溢出全 0(改前 /admin 在 320 还溢 34px);桌面 1440 与改前一致(−15 是滚动条宽)
- 全站 7 个页面都没有低于 11px 的文字

说明:A/B 与 C/D 本想拆两笔提交,但 app.css / layout.html / view.go 里两批改动交织,按文件拆不开、
按 hunk 拆又容易做出「中间那笔编译不过」的历史,所以合成一笔、在消息里分段。
2026-10-09 17:01:08 +08:00

14 KiB
Raw Blame History

rond-with-you 硬规矩(索引)

全文:.workbuddy/memory/MEMORY-details.md(38 节);验证机制:技能 rond-fix-verify;备份包解析/导出:技能 rond-backup-parse。本文件只做关键词索引——新规矩写 details,别堆这里。

⚠️ 本文件按字符数注入,硬上限 MAX_MEMORY_CHARS = 1e4(WorkBuddy app.asar 里 getWorkingMemoryContent;中文 1 字算 1,不是按字节)。超了会被 substring(0,1e4) 截断并写明「先整理再干活」——即整段尾部静默丢失。日常日志与本 details 不参与注入,可以随便写。现值见 wc -m,留 20% 余量。

落库 / 精度(details 一、四、五、十六、十九、廿八A)

  • 建地点+到访同一事务;新行号只走 nextSrcPK+clearStaleRaw,禁 MAX(src_pk)+1。seq_test.go
  • 行程派生列同导入(validateMovementForm):from/to_place_id(留空=绕过区域过滤直露访客)、distance_km、duration_min。
  • 表单精度低于真值 ⇒ 只改备注也静默改时间/坐标:keepStoredTime/resolveMovementEnd/keepStoredCoord;改到访时间须 clampVisitMovements 夹行程。
  • src_pk 只单数据集内有效;跨备份键=ZIDENTIFIER_/ZUID_;contentHash 别用 SrcPK|Arrival。
  • 坐标留档 coord_source/coord_sys/src_lat/src_lon;换算只走 geo 包与 /api/coords。
  • 脏数据:appleTime 超范围浮点转 int64 回绕非饱和(先卡秒数);validCoord 非法归 (0,0)。dirty_test.go
  • 聚合刷新(saveVisitTags/refreshPlaceAgg/refreshDatasetAgg/activityFlags)必须返回 error。agg_error_test.go
  • 收藏切换走单语句 UPDATE … RETURNING bookmarked。visit_bookmark_test.go

rond 导出(details 二、廿六)

  • 铁律:PG 覆盖管理列、entity_raw 还原其余列;基线已有行保真机原值(keepNilCols+known[pk])。
  • 新行只补 ZRAWLATITUDE/LONGITUDE、ZVISIT.ZIDENTIFIER_/ZACTIVITY.ZUID_(确定性=表名+src_pk)、ZICON_、ZLASTARRIVALDATE_ 哨兵 -63114076800。
  • ZMOVEMENT.ZTYPE_ 不能 NULL、别当管理列(会洗掉 type=4);queryTransports 必须 GROUP BY transport_src。
  • rows 收尾必须 drainRows(先 Err() 再 Close())。parse_rows_test.go
  • 扫进裸 bool 的列必须先 COALESCE:weather.is_daylight 漏写 ⇒ NULL ⇒ 整包拒收(details 卅C)。
  • 核对导出包别按留档 UUID 找行:站点新建行会被补派生 UUID(coreDataUUID)。
  • 换基线(底座换代,/admin/baseline):站点数据一行不动,只换底稿。判据=到访 UUID 重合率(matched==0 硬拒)+ 干跑导出;记档 datasets.baseline_*,ensureBaseline 必须先看记档;留档名必须防重名(秒级时间戳会让回滚覆盖掉刚生成的留档)。baseline_swap_test.go(details 卅三)

迷雾 / 遮罩(details 三、九、十、廿二)

  • .fwss=zip、条目 zlib;Model/*/=头 32768B+每块 515B;Model/~/、Model/#/ 原样写回;清空/替换都要连 fog_raw_files。
  • 导入三段式(preview 暂存 → 差异页现算 → ?stash= 落地);带 stash=urlencoded、无 stash=multipart 两条都留。
  • fog_masks 圆心 GCJ-02+半径,判定前换 WGS-84 网格;遮罩只对访客;stats.Filter 有两套 where(visitsWhere+movementsWhere),漏一个=「到访藏了里程还在」。
  • 瓦片须与底图同 crs;带地图页须 p.HasMap=true。
  • tileDigits 补前导 0;tileName 尾缀 len(s)-2 单数字编号时为 -1 ⇒ panic;解码守卫 len(name)<8。
  • fogIndexV 必须 atomic.Pointer(普通指针=真 data race)。fog_race_test.go(-race)

前端 / 模板 / CSS(details 六~八、十七、廿三、廿八D、卅七)

  • ⚠️ 对访客公开的块,措辞必须按身份分支({{if .IsAdmin}}我的…{{else}}站长…{{end}}): 写模板时人站在站长视角下笔,「我的结论 / 你自己 / 我推荐的地点」露给访客就没人读得懂。 照 place.html 投票区与 rond-map.js 气泡的写法。门禁 TestVisitorPagesUseThirdPersonCopy (以访客 ctx 渲染 /、/stats、/report 逐个断言禁用词,并确认站长那份仍说「我的」)。
  • 改 Go 必须重启;模板/CSS/JS 只在 debug=true 热加载(AssetV 启动哈希)。⚠️ 新模板+旧二进制=「渲染到一半仍 200」;启动自检 smokeRender 会拒启(四十一)。
  • 字阶/圆角/阴影只走令牌,裸 font-size: 13px 有静态守卫 TestNoBareFontSizePx;卡片三档 .card--hero/--quiet。⚠️ 改根字号要查 Leaflet(.75rem 会跟着漂),(四十二)。
  • 类型色走 csscolor/csssoft(必须返回 template.CSS,否则 var() 被换成 ZgotmplZ 且不报错);这套色连 3:1 都不到,只能当圆点标记、别当文字色。窄屏导航收进 .nav-menu,顶栏 --z-nav 必须高于地图浮层(四十三)。
  • 拼查询串走返回 template.URL 的方法(PlaceLink/EditPlaceLink/MapLink/EditLink),勿手工转义。
  • 窄屏覆盖写 app.css 末尾那块;宽表堆叠类名成对 class="tb tb-stack";触控 44px。tb_stack_test.go
  • 加表格列:PlaceCols/scanPlace 成对、新列追加最后;CJK 挤换行只抬该表 min-width。
  • ⚠️ 模板 JS 别用「heredoc+python」写 \n(<script> 语法错、交互全哑而模板测试照过);用 Edit,改完 CDP 验 JS 报错为空。inline_script_test.go
  • 只在访客复现的 bug:先比「无 cookie 原始 HTML vs CDP 后 DOM」;别让 JS 从 {{if .IsAdmin}} 块读 id。
  • 异步回包一律发序号作废:var seq=++xxxSeq; 在 if(!on) return; 之前,回包首行 if(seq!==xxxSeq) return;。map_async_cancel_test.go
  • window.localStorage 一律经 lsGet/lsSet(隐私模式裸访问会抛);previewQueue[id] 须在 if (live) 之前排队、回调消费后删。

编辑页 / 重复到访(details 三十)

  • 「重复到访」卡片:判定与 tools/dedup_site_visits.py 同语义、UUID 是前提、基线对不上就不给按钮;改指合并标记只动本来就有的键(create_if_missing=false)。dup_visits_test.go
  • 已并入 / 子段(ZMERGEDTO/ZPARENT)只在读侧收口,判据唯一一份 stats.SoloVisitSQL;天气与位移不收口;导出侧一字节不动。收口点清单见 details 卅G。
  • 相对计数断言会假绿(变异让基准值一起膨胀),必须写定义性不变量;render 是流式直写,code==200 不等于渲染完。
  • ⚠️ ingest 地点聚合回填必须排在 insertEntityRaw 之后(判据读 entity_raw)。

活动 / 类别(details 廿四、廿五)

  • 活动是表,类别不是——类别只是 places.poi_category 字符串列;显示名绝不写进该列(TestCategoryLabelNeverTouchesValue),走 categoryLabel(v,custom),Go 侧算好塞。
  • 站内新建行 entity_raw.raw IS NULL;ZCATEGORY_ 真机只有 MKPOICategoryXxx 枚举。
  • 类别候选:编辑/照片页=库里在用 GROUP BY;补全页=在用 ∪ 静态表;下拉留「其它(自己写)」,默认「不动」。

归属口径 / 防复发(details 卅五)

  • ⚠️ 铁律:这份记录能不能从备份重新导入? 能 ⇒ 可挂 dataset_id … ON DELETE CASCADE (places/visits/movements/weather/activities/tags/entity_raw);不能 ⇒ 只留 dataset_id 当出处、不许挂外键 (place_images/place_notes/place_votes/topics)。挂在备份里没有的表上 = 删数据集就永久丢失。
  • 两道门禁都钉着它:运行时查 pg_constraint(表必须在白名单里,反向也查)+ 静态扫 schema.sql (新建的库走 CREATE TABLE,只看运行时会有盲点)。dataset_ownership_test.go
  • 体检常驻后台首页:库里 N 张 / 本地盘上 M 个 / 落单 K 个(imageHealth)—— 事故的本质是「行与对象悄悄对不上、没人发现」,让它当场可见而不是等照片消失才追。

跨备份 / 附属数据(details 十九~廿一、卅四)

  • 预演按 VisitUUID/ActivityUUID 对齐、只读;raw #>> '{ZIDENTIFIER_,$blob}' 是 base64 须转 hex 比;地点只认坐标(PlaceCoordKey 6 位)。
  • 附属数据(备注/投票/图片)主键=place_key 坐标键(非 dataset+src_pk);Go 与 SQL 两套须逐位一致。
  • 站长结论(place_votes)与访客票(place_visitor_votes)是两码事:各存各表、各看各榜; 统计页拆成「我的结论」与「访客点赞」两块(votes.TopVisitorVoted,HAVING 只算有票的那个方向)。 ⚠️ 票表也按坐标键跨数据集读 ⇒ 用例必须按 place_key 清历史票,否则票数越跑越多。(details 卅六)
  • ⚠️ 附属数据不能挂数据集外键:place_images 曾按 dataset_id CASCADE ⇒ 删旧数据集=删图片行、文件却留在盘上(读取按坐标键、行按数据集级联,两套口径打架)。已摘掉外键;恢复走后台「图片挂载恢复」,认地点靠旧数据集留下的 baselines/<id>.sqlite(别清 baselines 目录);⚠️ 来源必须全部读进来合并(places → entity_raw 留档 → 该 sqlite 及每份 .bak)——换基线会覆盖底座文件,只看当前那份会整片认不出(实测 40 张只挂回 7 张)。image_recovery_test.go(details 卅四)
  • ⚠️ 恢复同时扫对象存储(R2/S3):media.ListObjects(ListObjectsV2 + SigV4,查询串先编码再签), 需 s3:ListBucket 权限;列不动时本地照救 + warning。专题同理:topics 已改成归人不归数据集 (user_id 过滤,内容仍照生效数据集算)。用户级数据靠 Dataset.UserID 定位。(details 卅四)
  • ⚠️ schema.sql 迁移块必须收敛且排在依赖表之后(插在 place_images 前:开发库无症、新库起不来)。
  • 合并 backup_merge.go:整个 withTx、不删除;新行 *_visit_src 写库里的 src_pk;必须 writeRawRow;删地点前先查有无到访(CASCADE)。

后端 / 性能(details 十、廿八C)

  • findSourceArchive 先按 size_bytes 筛再算 sha256(pickArchiveBySha)。池 8/8 + db.WarmUp(d,6);页面内查询 runJobs(≤6);别用 fog.Index.All();改遮罩须 masks.invalidate()+fogTiles.clear()。
  • placeCols 子查询必须带 vN.dataset_id=p.dataset_id。stats/placecols_test.go(按子查询分段判)
  • multipart handler 直接 ParseMultipartForm(r.ParseForm() 毒化字段);之前必须 MaxBytesReader。五个上传口:备份/GPX/迷雾/照片/地点图片。
  • 包了 ResponseWriter 必须实现 Unwrap()(gzipWriter/headerSent);recover 写错误页前须看 headerSent。server_error_test.go
  • 「先落盘再解析」的上传 handler,落盘后每条失败路径都要删半成品。upload_cleanup_test.go
  • 批量勾选走 *Bulk + p.src_pk = ANY($3::bigint[])(pgx 直接吃 []int64);全局内存缓存必须带上限(fillupCache FIFO 淘汰最旧 1/4);同一查询别在一个 handler 里查两遍。admin_notes_bulk_test.go+fillup_cache_test.go+stats_weekday_once_test.go
  • 加 setting 字段改三处:Default()/Load()/settingsSubmit()。

安全 / 跳转(details 廿八B)

  • 跳转唯一判据 safeInternalPath(view.go);四处共用。redirect_target_test.go(含禁弱判据静态扫描)
  • cookie Secure 按请求判断(cookieSecure),否则本机 http 与 CDP 都登不进。
  • CSRF 只加同源 Origin/Referer 守卫(csrf.go):仅写方法、Origin: null 拒、两头都缺放行。
  • clientIP 只认 XFF 最后一段(须 netip.ParseAddr);⚠️ 投票匿名身份=hash(cookie,IP)⇒改取法即改身份。
  • GPX 按属主收口:/api/gpx 取生效数据集 user_id 再 WHERE user_id=$1;adminGpxDelete 查 RowsAffected()==0。gpx_owner_test.go
  • 下载名走 downloadDisposition(ASCII 兜底+filename*=UTF-8'');四出口都经此。download_name_test.go
  • writeRawRow 必须上抛错误让 withTx 回滚。两步验证两闸都要:loginGuard(键 "2fa:"+用户名)+CodeReplay.Consume。twofa_guard_test.go+code_replay_test.go

口径 / 404 / 验收(details 十一~十三、廿七、廿八E、卅二)

  • ⚠️ 跑全量验收前:① VACUUM (FULL, ANALYZE)(死元组涨到 3.3GB 后级联 DELETE 变 IO 爬虫,看着像 hang);② 库内必须有 is_active 数据集,否则 internal/stats 6 用例假 skip。跑着时别并发跑定点测试。
  • 区域判定两端各过一遍再 AND(同 regionSQL);movementsWhere 也必须两端各判,不能用 EXISTS(... IN (from,to) ...);比较列一律 COALESCE(col,'')=$n。region_consistency_test.go
  • 其余(累计停留口径、Filter.Verdict 下沉、404 分档、验收四件套、覆盖率 -coverpkg)见 details 卅二。

备份合并 / 导出链路(details 卅一 + 廿一)

  • 对齐键:到访=ZIDENTIFIER_、地点=PlaceCoordKey(坐标 6 位,不认名称)、活动=ZUID_;合并前必须重算差异。
  • 地点配对只有一份实现 rond.MatchPlaces(差异与合并共用);同名兜底必须有距离上限(nameMatchMaxKm=1.0)。
  • 差异页默认 radio 是「用实机」;写合并测试要照这个提交(抓 value="backup"),否则测的不是真实点击行为。
  • 自检不变量:合并后没有任何地点被搬动 >1km、也没有任何到访离它的实机地点 >1km。
  • 三条铁律:① app 只靠 Z_PK 解引用地点/活动 ⇒ 导出的编号必须与数据集基线同一套 ⇒ 基线必须是手机包;② rond 导入是整库替换 ⇒ 导入前先清空 app 数据;③ 站点数据是超集 ⇒ 用「手机导出作新数据集 + 旧站点导出当备份合并进去」对齐;ZRAW 这类从源包抄来的计数会越界,靠导出侧夹取兜住。