Files
tamabox/.workbuddy/memory/2026-10-19.md
T
2026-10-19 16:16:07 +08:00

32 KiB
Raw Blame History

2026-10-19

部署后复测(强制严格档那批)

  • 规则页返回首页已居中:线上 <div class="uk-margin-large-top uk-text-center"> ✔
  • 验证码弹窗按钮已缩短:线上 >送信< ✔(旧的「認証して送信」已消失)
  • 箱主面板 /user/questions:限制期条幅 + 「查看社区规则」链接都在 ✔
  • 严格度锁定(admin6 当前 L2):
    • /user/profile 出现「处置期间审核严格度已锁定为最高档,解除后自动恢复你的原设置」✔
    • select[name=strictness] 与「保存审核严格度」按钮均带 disabled ✔
    • 直接 POST /user/moderation-strictness/update?tab=security + strictness=lax → 302 回 profile, flash 仍是「已锁定」,且选中档仍为 strict(说明未写入)✔
  • 踩坑:表单 action 是 /user/moderation-strictness/update(没有 /profile 段);用错路径返回 302 到 /, 容易误判成「被中间件拦」。另外每次 POST 都要重新取 _csrf,用上一页(已跳首页)的 token 会 400。

记忆维护

MEMORY.md 一度 19KB 被注入截断,已重写压缩到约 4.9k 字符(8.1KB),保留构建/行尾/embed/审核/处置/CLI 等硬约束, 删去已在日志里的过程细节。

CLI 文档 + 全量代码审阅(同日)

新增 docs/cli.md:命令总览、reports 三种用法与口径、restrict 级别语义/副作用、unrestrict、 hide-public、ban/restore、典型处置流程、排错表、明确不做的事。

审阅中发现并修复(全部本地临时库实测):

  1. hide-public 举报闭环静默失效(真 bug):build() 带 is_private=false,在 Update 之后拿它当子查询恒为空集 → 隐藏成功但 content_reports 仍 pending。改为 Update 前 Pluck("id") 快照,再按 ID 更新/标记。 实测:3 条 pending → accepted,未波及其他箱主。
  2. 整箱分享卡未拦截:ShareCard 直接走缓存,L1–L3 仍能生成整箱图。已在缓存查询前加拦截(fail-closed)。
  3. L2 通知邮件误导:强制私密后仍给提问者发「你的提问有回答」,链接点开 302。改为 !forcedPrivate 才发。
  4. L2 写入顺序:SetPrivate + AnswerByID 两条语句有「公开+已作答」窗口,第二条失败还会留下「私密+无回答」。 新增 Questions.AnswerByIDPrivate(单条 UPDATE 同时写 answer/is_read/is_private),并补测试 TestQuestions/AnswerByIDPrivate。
  5. ListAPI 绕过 Pager:L4 停用与页面访问密码只挡 HTML,/api/v1/user/{domain}/questions 仍能整页拉。 已补两道校验(均返回「用户不存在」,不暴露原因)。页面密码可被 API 绕过是升级前就存在的问题。
  6. L1 曝光残留:GetShowInHome 的 Count 未过滤(分页总数虚高、末页空白)、热门标签查询未过滤。已补同一子查询。
  7. 性能:ActiveLevel 由两次查询改一次(取最高 rank);新增 moderation.EffectiveStrictnessFor(level, raw), 发布路径上不再重复查库(原先一次发布最多 4 次处置查询)。
  8. 证据包口径:分母是「累计公开回答」不是窗口产出 → 文案加「累计」;新增 待处理 N / 已处理 标记, 避免把已闭合的举报误读成还挂着;补 q.deleted_at IS NULL;--stats 说明最长回溯 30 天;CLI 对 active 做 nil 守卫。

验证:go build ./... EXIT=0;db/i18n/templates 测试全绿;CLI 全流程(restrict L3 → 证据包 → unrestrict)实测通过; 临时库已 DROP。

遗留(未改,已向用户说明):L4 不覆盖 /vote/{token};restrict 没有 --days 上限;处置不发邮件给箱主(只有登录后条幅); abuseAlertCooldowns 进程内 map 无清理(重启即失效)。

第二轮:处置通知邮件 + 超长确认 + 冷却表清理

  • 新增 templates/mail/restriction-notice.html + mail.SendRestrictionNoticeMail(RestrictionNotice): 文案由调用方按箱主自己的语言(users.language,空则 conf.App.DefaultLang)预翻译后传入, 模板本身单语言。不含 --reason 原文,只有级别名/到期/规则链接/申诉渠道。 i18n 新增 10 个键 ×4 语言:mail.restriction.*(6)+ restriction.level.L1..L4。
  • restrict:新增 --force(--days > 90 必填,否则直接报错)与 --no-notify;交互模式下超长处置 还要再输一次级别名(--force --yes 视为两次确认)。发信失败只 warn + 终端提示,不回滚处置; 发信加了 20s 超时(gomail 无 Timeout 字段,裸 Dial 会等系统 TCP 超时)。
  • abuseAlertCooldowns 加惰性清理(>1024 条时删除已过 24h 的项)。
  • 顺带:/user/questions 限制期条幅改用 restriction.level.* 的名称(「收窄公开」)而不是裸 L2。

验证:真实 SMTP sink 实收两封(ja 与 zh-CN),标题/级别名/到期/规则链接均正确、--reason 未出现、 链接未被 ZgotmplZ 转义;--days 120 无 --force 被拒;黑洞 SMTP 下 23s 内返回(未阻塞两分钟)。 go build ./...、go vet、db/i18n/templates 测试、checktpl 全绿。

遗留:unrestrict 不发通知(只解禁);restrict 对 --days 无上限(设计如此)。

第三轮:解除处置也发通知

  • 复用同一封模板 mail/restriction-notice.html:appeal 段改为 {{if .appeal}} 可选, 解除通知不传该字段就整段省略。
  • 新增 i18n 键 ×4:mail.unrestrict.heading/body/note(body 带域与级别名; note 明确写「处置记录保留用于审计、私密内容不会自动恢复」)。
  • unrestrict 新增 --no-notify;撤销前先 ActiveLevel 取级别(撤销后读不到), 撤销后按箱主语言发解除通知。affected==0 直接返回、不发信。
  • 抽出公共件:sendNotice(20s 超时)、reportNoticeResult(统一成功/失败输出)。
  • 文档补充:unrestrict 的 --no-notify、解除通知内容、以及到期自动解除不会发信(无调度器)。

实测(本地 SMTP sink,2 封):处置信含级别名/到期/申诉段;解除信含「已解除」+ 私密内容提示、 无申诉段;两封都不含 --reason。--no-notify 两处都不发信;无生效处置时不发信。 go build ./...、db/i18n/templates 测试、checktpl 全绿。

上线前最终验收(部署后实测)

  • 公开页:/ /rules /restricted /deactivated 全部 200;/rules 四语言标题正确。
  • 箱主面板(admin6,L2):条幅显示「收窄公开」+ 规则链接;严格度面板锁定提示 1 处、下拉 disabled。
  • L2 写入路径实测(关键,因为改成了 AnswerByIDPrivate 单条 UPDATE): 对未回答题 q95 作答 → 302 回题目页 → flash「已保存为私密:当前处于平台处置期,回答不会公开」→ 匿名访问 302。
  • 公开列表 API /api/v1/user/{domain}/questions 正常返回 {code,data:{data,next_cursor,has_more}}(新增的 L4/密码校验没有打断正常路径)。
  • 整箱分享卡:受处置箱主 302(拦截生效);未处置箱主 500,原因是测试站未配置字体(分享图生成失败:字体文件未配置或无法加载),与本次改动无关。 这一点反而证明拦截位置在生成之前。
  • 未能自测:CLI 发信(restrict/unrestrict 通知)需要在服务器上跑命令,只能由用户执行。

注意:admin6 现有测试题 73/88/89/90/94/95;其中 95 已被我回答并存为私密(内容带 restrict self-check)。 admin6 的 L2 于 2026-10-19 21:29 自动到期。

邮件模板统一(用户反馈:页脚与其它邮件不一致)

  • 原因:restriction-notice.html 是我新写的,页脚用了自造的 © 2026 TamaBox,与项目其它 11 封邮件的 「此邮件由系统自动发送,请勿直接回复。
    {{ .year }} 狼的提问箱」不一致。
  • 处理:整封模板按 question-reported.html 的结构重写(24px 标题 + {{.time}} 副行、同样的卡片/页脚/外间距), time 由 mail.SendRestrictionNoticeMail 统一注入(调用方不用各自拼时间)。
  • 实测(本地 SMTP sink):页脚文本与 question-reported 完全一致,© ... TamaBox 已消失,头部时间行存在。
  • 注意:站点名在所有邮件模板里都是硬编码「狼的提问箱」,不是配置项;改站名要一起改 12 个模板。

页脚精简(移动端一行放不下)

  • 去掉「盘」链接(https://share.shiroko.one,zh 文案就叫「盘」),并删除已无引用的 site.footer.drive 键(四语言同步删,保证 TestDictionaryComplete 词条数一致)。
  • site.footer.rules:zh-CN「社区规则」→「规则」,zh-TW「社群規則」→「規則」(en/ja 本就短)。
  • 现在页脚一行:规则 | 找站长 | 公告 | 聊天 | 状态 | 主页。
  • 注意:templates/base/footer.html 用 | 分隔,行首行尾各带一个,删整行时不会留下悬空分隔符(已核对)。

页脚改两行(用户改主意:盘恢复)

  • 恢复 https://share.shiroko.one「盘」链接与 site.footer.drive 键(zh-CN 盘 / zh-TW 盤 / en Drive / ja ドライブ)。
  • site.footer.rules 文案恢复:社区规则 / 社群規則(en/ja 不变)。
  • 布局两行(靠 <br> 断行,不用额外 CSS): 第一行 社区规则 | 找站长,第二行 盘 | 公告板 | 聊天室 | 监控板 | 导航页。
  • 键插在 site.footer.rules 之后(组末),空格数按同长度键复制,gofmt 对齐不变;dict 仍 CRLF。
  • 验证:build / checktpl / i18n / templates 测试全绿。

页脚改为「从少到多」四行

版权行与存活计时移到最后两行(原来在最上面),整块按文字量递增: 社区规则 | 找站长 → 盘 | 公告板 | 聊天室 | 监控板 | 导航页 → Powered by NekoBox | ICP → 提问箱已成功存活:…。 实现只调整了 <br> 的位置,仍共 4 个 <br>,所以块高与顶部空行跟原来一致(未新增占位)。 注意:box_runtime 的脚本按 id 取元素,移动位置不影响;但 <br> 数量别随意增删,否则页脚高度会变。

前端样式体检(只出结论,未改代码)

量化事实(脚本统计 templates/**/*.html):

  • 全站 0 个 .css 文件,样式全部内联:base/header.html 内含 17.5KB CSS + 10.6KB JS(模板总 42.5KB), footer 3.0KB JS;全模板共 24 个 <style> 块,user/profile.html 自身还有 11.2KB CSS + 22.8KB JS(总 62KB)。
  • --tb-* 设计变量 只在 html[data-theme="dark"] 里定义,light 主题没有 :root 一套; 调用处全靠 fallback(#e9e9e9 / #e5e5e5 / #fafafa / #333 / #666 混用),explore 等页面还写死色值。
  • 深色模式 FOUC:applyTheme(getTheme()) 在 body 内、<nav> 之后执行(header.html:812), 深色用户刷新时顶栏会先白闪一下。修法:把 theme 初始化内联脚本提到 <head>。
  • 结构:uk-container-xsmall(750px)由 header 统一输出,仅 explore/vote/chat 页面自套 small; question/list.html 仍用 <center>、内联 style="min-height:260px",用户名/简介颜色由用户自选压在背景图上,无 scrim → 可读性无保障。
  • 首页 home.html 是纯 UIkit 卡片 + <hr> + style="color: red" + <pre> 统计 + 首页挂 Waline(250 行 locale)。

建议分档(未实施):P0 = light/dark 双套 token + 圆角/阴影/中文字体栈 + 首页重排 + 背景图 scrim; P1 = header CSS 抽成 static/css/*.css(注意 embed.go/static/fs.go 都是显式清单)+ FOUC 修复; P2 = 图标统一(UIkit icons 取代 emoji/位图 png)、标签减量、viewpoint/meta 去重。

P0 前端美化(已实施)

用户拍板:卡片结构不变,只加圆角和柔阴影;先做 P0,不抽 CSS 文件。

改动文件:

  • templates/base/header.html
    • head 顶部新增主题初始化内联脚本(深色用户不再白闪;原 applyTheme 在 body 里、nav 之后执行)。 顺手删掉 body 脚本里的 applyTheme(getTheme()),只保留切换逻辑。
    • 新增设计令牌块::root 亮色 + html[data-theme=dark] 暗色,同名 14 个(bg/surface/surface-2/surface-hover/ border/text/text-secondary/text-muted/primary/primary-weak/radius/radius-sm/radius-xs/shadow/shadow-hover/font)。 原暗色块里的 7 个变量定义已删除(现在统一在令牌块)。
    • 新增基础质感层:html+body 底色与字体栈(system-ui + 苹方/冬青黑/雅黑)、卡片 12px 圆角 + 双层柔投影、 按钮/输入 8px、标签 6px、alert 10px、modal 14px、导航栏白底、:focus-visible 描边、tabular-nums、 prefers-reduced-motion。
    • .tb-cover:封面遮罩;默认浅色(配 .uk-light 的黑字兜底),.tb-cover-dark 为深色。
    • .tb-img-uploader 上收到 header(原来 item / new-question 各写一份内联 style,写死 #f8f8f8 → 深色下露白)。
  • templates/home.html:整页重排为 hero(标题+一句话+探索/规则按钮)+ 3 张功能卡 + 域名迁移公告
    • 提醒列表 + <details> 折叠更新日志 + 3 个数字统计卡(.SiteStats 数值字段,30 分钟 JS 刷新改为按字段更新)。 全部原有信息保留;页内 style= 归零。
  • templates/question/list.html:封面加 .tb-cover(仅当有背景图),新增 12 行 JS 按用户名颜色亮度 决定遮罩方向;min-height:260px 内联改为 .tb-profile-cover。
  • 6 个页面样式块里的硬编码色值收敛到令牌(explore/question-list/profile/list/item/vote-page,共 ~84 行), CRLF 文件用字节级替换(未改动行尾)。

验证:go test ./templates/... ./internal/i18n/... 全绿;go build(overlay)通过。 新增 templates/zz_preview_test.go:TEST_PREVIEW=1 go test ./templates -run TestGenPreview 产出 outputs/preview-{home,box,explore}-{light,dark}.html,用无头 Chrome 截图核对过亮暗两套。 ⚠️ 本机 Chrome 新版 headless 会挂住,必须 --headless=old,且页面外部资源要本地化 (outputs/preview-assets/,从 cdnjs.snrat.com 取 UIkit 3.3.3;cloudflare/jsdelivr 在本机不通)。

封面遮罩 P0 缺陷修复(重要)

症状:.tb-cover::after 遮罩不渲染,且完全不报错——截图对比像素只差 ±1(只看着像"没效果")。

根因:UIkit 3.3.3 自带 .uk-flex::after,.uk-flex::before{display:none}(优先级 0,2,0), 而封面元素是 uk-flex,我写的 .tb-cover::after(0,1,1)优先级不够 → 被压成 display:none。 定位方法:getComputedStyle(el,'::after').display 得到 none;再逐个 classList.remove 试 → 去掉 uk-flex 立刻变 block。最后在 uikit.min.css 里 grep 到那条规则。 修法:选择器写成 .tb-cover::after, .tb-cover.uk-flex::after 并显式 display:block。

实测效果(无头 Chrome 截图 + 像素测量,非目测):

  • 深色渐变背景 + 白字:封面底部亮度 0.229→0.102,白字对比度 3.77→6.89(过 WCAG AA 4.5)
  • 线上默认背景(box_background.png 2048×1152,接近全白)+ 黑字:0.912→0.971,对比度 19.25→20.42, 即对默认背景几乎没有可见影响
  • 亮度判定 JS 已验证:#fff 会加 tb-cover-dark,#000 不加。

教训:CSS 改动不能只看"规则写上了/类加上了",必须量像素。伪元素 + UIkit 工具类组合要在计算样式层验证。

账号背景图的真实链路(回答用户提问时查证)

  • 注册时把 [upload] default_background 一次性复制进 users.background(route/auth/register.go:51), 不是每次渲染兜底 → 之后改配置不影响存量用户。
  • 页面只用 .PageUser.Background 渲染 data-src;为空则无背景图、无遮罩。
  • 分享卡是另一条路:route/question/share.go:345 用 user.Background,为空时回退 conf.App.ShareBackground。
  • 线上 box.shiroko.one/_/tamakyi 实际值 = https://r2.337845818.best/tamabox/box_background.png。
  • 预览里那张"渐变图"是 zz_preview_test.go 造的测试数据(data: URI 渐变),不是产品行为; 顺带发现 Go 模板会把 data: URI 在属性上下文里替换成 #ZgotmplZ(真实 http URL 不受影响)。

设置页「防护设置」缺白底(用户报)

根因:其它面板都包在 <section class="tb-pcard">,只有内容防护面板(profile.html:842)是裸 div.profile-panel, 直接露出页面灰底 --tb-bg。修法:包一层 section.tb-pcard,并把 .tb-pcard 的 background:#fff 改成 var(--tb-surface)(删掉随之冗余的 dark 覆盖)。 实测(计算样式):account 与 security 面板 bg 亮色均 rgb(255,255,255)、暗色均 rgb(36,40,48),圆角 10px,完全一致。 顺手把 security 加入 render_test.go 的面板渲染测试列表(原来只测 account/question/chat/vote)。

⚠️ 教训:用脚本批量缩进/hash 插入时先 dry-run。这次第一版脚本把「空行」也当成需要写 <section> 的行, 一次性插了 5 个多余标签;靠立刻 sed 复查发现并回滚。

P1 第一步:CSS 抽离(已完成)

  • 新增 static/css/base.css(23KB,含注释;gzip 后约 6KB),内容 = 原 header 里的 设计令牌 / UIkit 补丁 / 深色覆盖 / 质感层 / 顶栏响应式 / 标签 / Markdown / 进度条 / 移动端底栏。
  • header.html:内联 <style> 从 17.5KB 降到只剩 1 个(算数验证码,跟着 {{ if CaptchaType }} 条件渲染); 改为 <link href="/static/css/base.css?v=20261019a">,位置在 UIkit 样式之后(保证覆盖顺序)。 单页 HTML 42.5KB → 30.9KB。
  • static/fs.go 的 //go:embed 显式清单加 css/base.css(漏了就是运行期 404 + 全站无样式,编译期不报错)。
  • 静态缓存沿用既有 CacheControl: public, max-age=86400;靠 ?v= 版本号破缓存。

验证方式:改动前后各截一次同样尺寸的截图做像素比对。 home-light / home-dark / explore-light 三页差异均 0.024%,bbox 只有 (11,56)-(29,74) 一个 18×18 方块 = 导航栏随机动物 emoji(每次加载都变),不是样式回归。go build + 模板/i18n 测试全绿。

测试环境复测(P1 CSS 抽离 + 防护面板白底,已上线)

  • GET /static/css/base.css?v=20261019a → 200 / text/css / 23088 字节,与本地 static/css/base.css 逐字节一致 / Cache-Control: public, max-age=86400 / gzip 后 7109 字节。
  • 首页 32747 字节(改动前 52747),gzip 10534(改动前 13642);页面内联 <style> 只剩验证码 + 首页自己的样式块。
  • 设置页(登录 admin6):data-profile-panel="security" 下已有 <section class="tb-pcard">; 用线上 HTML + 本地 CSS 复算样式 → account / security 两块 bg=rgb(255,255,255)、border-radius:10px,页面底 rgb(247,248,250)。
  • /_/admin6 公开页:class="... uk-light tb-cover",:after 计算值 display=block, width=689px → 遮罩在真实部署上确实渲染了(UIkit .uk-flex::after{display:none} 的坑已解决)。 该用户用户名为 rgb(0,0,0),走浅色遮罩(符合预期)。
  • 各页 200:/、/explore、/rules、/user/questions;/user/vote-form 无参 302 属正常。
  • 首页首次访问总字节略增(10.5+7.1 vs 13.6KB gzip),但 CSS 跨页缓存后每页少 3.1KB,且服务端少渲染 17.5KB 模板。

专属测试账号(用户指定)

  • https://box-dev-api.337845818.best/_/admin6,邮箱 [email protected],密码 adminadmin。
  • 登录需算数验证码,curl 流程:GET /login 取 _csrf(同时存 cookie)→ GET /api/captcha/generate 拿 {id, question} → 算答案 → POST /api/captcha/verify(头 X-CSRF-Token,体 {id, answer})→ POST /login(email/password/_csrf/g-recaptcha-response=id:answer)→ 302 到 /,cookie 已带登录态。

P2 一致性收尾(已实施,未部署)

  1. 导航图标:🌐🌙☀️ emoji → 内联单色 SVG(stroke: currentColor,跟随主题/悬停色)。 ⚠️ 原计划用 UIkit icons,实测 UIkit 3.3.3 的图标集里没有 moon / sun(只有 world), 照写会渲染成空白按钮——所以三个图标全部改成内联 SVG,也不依赖 JS。
  2. 标签:投票类型 20 处 uk-label uk-label-primary(实心蓝)→ .tb-chip 轻量胶囊; 探索页热门标签原来的 5 色轮换(success/warning/danger/primary/secondary)也统一成 .tb-chip。 箱主面板 question-list.html 另 6 处字节级替换(CRLF 保留)。 保留:置顶/私密/被举报/已读 这些语义状态仍是彩色 uk-label(有含义,不该降级)。
  3. 遗留写法:<center> ×2 → div.uk-text-center;占比条从 div.uk-progress > div.uk-progressbar(style:width) 改成 UIkit 3 原生 <progress class="uk-progress" value=".." max="100">,删掉 25 行 UIkit2 兼容补丁, 只保留 10px 高/6px 圆角的外观收窄。测试 percent_check_test.go 的断言同步改成 value="66" max="100"。
  4. meta:删掉重复的 msvalidate.01(保留 head 顶部一份)、viewport 补 initial-scale=1。

踩坑:暗色主题图标原来写 display:inline,span 里放块级 <svg> 会让导航栏高度变化 → 整页下移 36px。 用行亮度曲线做互相关定位到 dy=36,改成 inline-flex 后复测差异回到 0.059%(只剩随机 emoji)。 教训:改图标尺寸/显示模式一定要量整页位移,不能只看局部截图。

P2 线上复测(2026-10-19 12:2x)

  • GET /static/css/base.css?v=20261019c → 200 / text/css / 23918 字节,与本地逐字节一致,页面已引用该版本。
  • meta:viewport 已带 initial-scale=1;msvalidate.01 只剩 1 份。
  • 图标:首页 emoji 🌐🌙☀️ 归零,<svg ... stroke="currentColor"> ×5(语言 1 + 桌面主题 2 + 移动主题 2); 计算样式暗色下 #theme-toggle .tb-icon-dark = display:flex、亮色图标 none; 导航高度亮/暗均 80px → 上次 36px 位移问题确认修好。
  • 探索页:.tb-chip ×25(全部有底色 rgb(244,246,248)、文字 rgb(92,99,112)、圆角 6px、字号 11.52px); 残留彩色 uk-label 4 个,均为置顶/私密等语义状态。
  • 结果页:/user/votes/{id}/results 原生 <progress class="uk-progress" value max>,计算高度 10px、圆角 6px; 遍历 10 个结果页 uk-progressbar 全部为 0。
  • 巡检 / /explore /rules /login /register /_/admin6 /_/admin3 均 200。

P2 补漏(已改,待部署)

复测时发现 结果页与参与页的题型标签仍是实心彩色 —— 那两处的 class 由 Go 模板函数 fieldTypeLabelClass 生成,我上一轮只按字面 class="uk-label uk-label-primary" 搜索,漏了它们。

  • templates/user/vote-results.html(2 处)、templates/vote/page.html(2 处):序号 + 题型 → .tb-chip; 状态标签(未开始/已结束/进行中/已参与)保留彩色,属语义。
  • 删除 internal/template/template.go 的 fieldTypeLabelClass(19 行,已无引用)。 顺带查明:该函数 text/textarea 走 return "",注释说"默认灰",实际渲染成 UIkit 默认蓝, 所以文本框标签一直是蓝的——注释与实现不符。
  • .uk-progress::-webkit-progress-value 圆角改成两端都圆(-moz-progress-bar 同步)。 ⚠️ 起因是误判:我先前测"右端方角"时像素扫描循环上界写成 range(110,830),恰好等于条宽, 于是右端永远"恒定不变"→ 假象。放宽到整幅宽度后两版端点曲线完全一致(830/832/833/834/833/832/830), 即没有视觉 bug。保留改动只为 Firefox 的 -moz-progress-bar 保险。
  • base.css?v= → 20261019d。go build + ./templates ./internal/i18n ./internal/template 全绿。

教训(重要):像素测量脚本的扫描范围必须覆盖整幅图,循环上界不能与被测对象边界重合, 否则会把"边界截断"读成"图形特征"。之前 3.77→6.89 那组对比不知是否也受此影响,需复查时再确认。

P2 补漏部署后复测(12:48)

  • base.css?v=20261019d 上线,24023 字节与本地逐字节一致。
  • 删掉 fieldTypeLabelClass 后无运行期报错:公开投票页 3 个(200,tb-chip 8/16/4,uk-label 各 1 = 状态)、 结果页 4 个(200,tb-chip 8/2/0/16,彩色 label 全 0,progress 7/0/10/19)、 草稿预览页 2 个(200,tb-chip 2/6)。
  • 巡检 / /explore /rules /user/questions /user/profile?tab=security /_/admin6 /_/admin3 全 200 无模板错误。
  • /user/votes/{id}/preview 对已发布投票 302 到 /vote/{token} 是设计如此(vote.go:1093),不是回归。
  • 截图核对:结果页「1 単選 / 2 多選」已为浅色胶囊,进度条 10px 圆角 6px;参与页字段头同样统一。

"移动端语言/主题按钮不见了"排查(结论:非缺陷)

实测(线上测试环境,--force-device-scale-factor=1):

  • 宽 360/390/414/640 → 顶栏 .tb-nav-utils = display:none,底栏 #tb-mobile-utils = flex, rect 0,639,W,46,语言 block、主题按钮 block(48×30,SVG 16×16),命中测试落在 mobile-lang-select。
  • 宽 700/768/980 → 底栏 none,顶栏 flex,右上角地球 + 月亮都可见(截图 outputs/t-nav-right.png)。
  • 页面级差异:/_/admin6 底栏只有月亮没有语言下拉 —— IsUserPage=true 让语言块整体不渲染(桌面同样没有)。
  • 生产 box.shiroko.one 仍是旧构建(页面无 base.css 引用,底栏是 emoji 月亮),移动端同样正常。

用户确认是忘了「移动端移到固定底栏」这个设计,不是 bug。 留下的真实缺口:箱主页(最常见的外部入口)完全没有语言切换,见 MEMORY.md 前端样式条目。

修正:箱主页无语言切换是设计,不是缺口

上一节把它记成"真实缺口"是错的。用户明确:箱主页属箱主个性化,以箱主配置为主, 语言入口不出现是有意为之(IsUserPage 门控)。 MEMORY.md 对应条目已改为"有意设计、别当 bug 修、要动先问"。

备份(用户要求)

../TamaBox-v4-mainline-20261019-1445.tar.gz(940K,302 条目,排除 .git/.workbuddy/outputs)。 P1 剩余两项(首屏跟随系统深色、首页 Waline 懒加载)用户明确先不做。

主题系统方案(只出设计,未实现)

用户诉求:箱主在后台选主题、仅对箱主相关页面生效;站点级默认主题在配置里;要一份可复用的主题底稿。

已确认的现状事实(设计依据):

  • 设计令牌只有 16 个,集中在 static/css/base.css 的 :root(亮) + html[data-theme="dark"](暗)。
  • <html lang> 在 templates/base/header.html:2,服务端渲染;主题属性可放在这里(无 FOUC)。
  • question.Pager(route/question/page.go:45)是 /_/{domain} 与 /_/{domain}/{id} 的共同中间件 → 打主题标记的唯一合适位置。
  • /user/* 与 /_/* 共用 IsUserPage(context.go:241 按 URL 前缀判定),所以「箱主相关页面」 ≠ IsUserPage,必须单独区分,否则设置页会被主题化。
  • static/fs.go 与 templates/embed.go 都是显式 embed 清单(新增主题 css 必须手加)。
  • 用户模型已有大量个性化字段(usernamecolor/introcolor/background…),主题作为 users.theme 同族最自然。
  • 分享卡是服务端绘制图片(route/question/share.go),不经过 CSS → 主题不覆盖它。

关键结论(写进方案的核心):令牌覆盖必须用 html[data-theme-style="x"](0,1,1,压过 :root 的 0,1,0)+ html[data-theme="dark"][data-theme-style="x"](0,2,1,压过 base 的 dark)。 只写浅色块会让深色模式被浅色令牌污染(同特异性 + 主题文件后加载)。

主题系统初版(已实现,未部署)

用户拍板:①作用范围跟随箱主配置(即 /_/ 箱主相关页面)②主题只做配色/图案/整页背景,不涉及布局 ③主题名可本地化 ④分享卡不参与。

实现:

  • internal/theme:注册表 + 三级回退。[theme] default/available → 白名单(slug 正则 + css 是否存在), Resolve(用户值):default→空串(不加载)/命中→该 slug/其余→站点默认。"" 与 "default" 语义不同: ""=跟随站点默认,"default"=箱主明确选基础外观。 设置页色块从主题 css 浅色块里抓 --tb-bg/--tb-primary 拼渐变(值必须过色值正则,防注入)。
  • users.theme varchar(32) default ''(AutoMigrate 加列)+ UpdateTheme 单列更新。
  • question.Pager 注入 Data["ThemeSlug"](/_/{domain}、单题、密码页、分享卡路由共用这一个中间件); header.html 在 <html> 输出 data-theme-style 并条件加载 themes/<slug>.css。
  • 设置页账号面板末尾新增「主题外观」卡片(独立表单 POST /user/profile/theme,不塞进 UpdateProfile, 避免与该面板其它字段互相覆盖);{{ if .Themes }} 包裹,未配置主题时不显示。
  • 静态版本号统一 conf.StaticVersion = "20261019e"(base.css 与主题 css 共用,改样式只改一处)。
  • static/fs.go 改嵌目录 css/themes(以后加主题文件不用再改 embed 清单)。
  • 主题文件:_scaffold.css(底稿,含 19 令牌说明 + 素材规格 + 特异性规范)、default.css(恒等,仅色块预览)、 sakura.css(示例,纯 CSS 网点图案 + 粉调)。
  • base.css:body 接 --tb-page-image / --tb-page-pattern / --tb-page-pattern-size 两层背景; UIkit 主色是编译期常量 #1e87f0,新增映射块把 a/uk-link、uk-button-primary、checkbox/radio、input:focus、 uk-tab active 接到 --tb-primary(基础主题值相同,默认外观零变化)。

实测(无头 Chrome,box 页 data-theme-style=sakura):

  • 浅色 primary #e5678f/bg #fff6f8/pattern 16px 网点命中;深色 #ff8ab0/#241a1f,卡片表面各自跟随。
  • 未选主题的 box 页:无 data 属性、无 themes link、主按钮仍 rgb(30,135,240) → 零回归。

踩坑:

  1. 第一版 sakura 页「发送提问」还是蓝色 —— UIkit 组件色写死,令牌覆盖不到,必须按组件映射。
  2. 预览里 {{ .StaticVersion }} 为空(预览夹具没这个键),导致按固定串替换主题 css 失败;改用正则。
  3. writePreview 用全文搜 data-theme="dark" 判断是否已加属性 → 命中内联主题 css 里的选择器,深色预览没生效; 改为只看 <html ...> 开标签。
  4. 脚本改 form/user.go 时把插入块写在结构体中间,拼出 } Language ...;靠 Read 复查发现并重写。

主题 follow-up(部署后反馈两处)

  1. /user/ 不生效主题*:根因是 ctx.Data["ThemeSlug"] 只在 question.Pager(/_/{domain})里注入, /user/* 完全没走这条链路。修法:internal/context/context.go 的 Contexter 里按 isThemeScope(path) && c.IsLogged 注入 theme.Resolve(c.User.Theme); isThemeScope 只认 /user 与 /user/(不能用裸 HasPrefix("/user"),会误吞 /username)。 新增 internal/context/context_test.go 守住路径白名单;page.go 的注释同步改。 公开页(/、/explore、/vote/{token})仍不跟随登录用户主题,保持站点默认。

  2. sakura 显示奇怪(用户选择:顶栏白与粉底割裂 / 卡片纯白太突 / 底色分布不均):

    • base.css 新增令牌 --tb-navbar(:root 里 var(--tb-surface),亮暗两条 navbar 规则都改成 var(--tb-navbar, var(--tb-surface))),主题可单独给顶栏上色。
    • sakura v2:底色改纯色 #fceef3(不用渐变——渐变会被读成"不均匀"), 顶栏 #fff6f9、卡片 #fffafc(暖白,不再是纯白硬切),网点从 16px/0.14 放宽到 24px/0.05。
    • 静态版本 20261019f;_scaffold.css 令牌说明 19→20。
    • profile.theme.hint 四语言文案改成「提问箱页面 + 本人后台生效,公开页保持默认」。
    • zz_preview_test.go 抽出 inlineThemeCSS(),并新增 preview-profile-sakura-*(后台页面也要在主题下检查)。

验证:go build ./...(须带 webp overlay)、i18n/theme/context/templates 四个包测试全绿。 渲染法:抓线上 /_/admin6 HTML → 把 base.css 指到本地新文件、主题 css 内联 → 无头 Chrome --headless=old 截图。