29 Commits
Author SHA1 Message Date
tamakyi b9060c982b feat(notify): 新增站级 Webhook 与用户级 Telegram 通知通道
在现有邮件通知之外补两条广播/点对点通道:

- 站级 Webhook([notify] webhook_url):新提问 / 新回答 / 新追问 / 新私聊 /
  新投票回复统一 POST JSON 到通用 Webhook(Discord / Slack / 企业微信机器人等)。
  消息体同时带 content(Discord 直接可用)与结构化 event/title/body/link 字段。
- 用户级 Telegram([notify] telegram_bot_token + users.telegram_chat_id):箱主
  自填 Chat ID,新提问 / 新投票回复点对点推送。

设计约束:通知是「尽力而为」旁路 —— 任一通道发送失败只记日志、不向上抛错,
绝不影响提问/回答的主流程;通道未配置时静默跳过。发送放在 shared.SafeGo 异步
执行,与现有邮件通知同一条路径。

6 处业务触发点接入 notify;私聊消息保留仅邮件(已有 5 分钟节流,避免广播刷屏)。
profile 页新增 Telegram Chat ID 输入框 + 四语言字典。app.sample.ini 加 [notify] 段。

测试覆盖 webhook 的 JSON 结构与未配置时的静默跳过。
2026-10-29 19:27:23 +08:00
tamakyi 4e574ac64a feat(insights): 箱主数据面板——访问量、提问趋势、处理漏斗
新增 /user/insights,让箱主第一次能看到「有多少人来过我的箱、提问处理得怎么样」。

数据来源:
- 新表 box_views:一行 = 一访客×一天×一箱,views 累加,唯一索引
  (user_id, viewed_on, visitor)。刻意不内嵌 dbutil.Model —— 软删除 + 唯一索引
  会让被删行永久占槽,且删过的槽位再也插不进去。
- 访客标识 = HMAC(日盐, IP) 前 32 hex:同日可去重(UV 准)、跨日不可关联(隐私)。
  代价是近 7/30 天 UV = 各日 UV 之和,页面上已写明口径。

写入是异步的:buffered channel 512 + 2s 批量 flush,队列满即丢(统计尽力而为,
不该因为记一笔访问就拖慢页面)。过滤箱主本人、空 UA、爬虫 UA、预取请求。
密钥复用 server.xsrf_key;每日清理 400 天前的明细。

页面(纯 CSS 画图,不引图表库):
- 今日/近 7 天/近 30 天 KPI + 上周对比
- 30 天 PV/UV 双柱趋势、24 小时时段分布
- 提问处理漏斗 + 回答率进度条 + 自动结论

聚合层 internal/db/box_analytics.go 复用 analyticsDialectFor 的时区处理,
与全站 stats 分开(那个不含单箱维度、按自然日缓存)。

测试:box_views 4 项(含「同批同键先内存合并」——PG 的 ON CONFLICT 不允许
同条 INSERT 二次命中同一行)、insights 的 pct/窗口切分/visitorID/爬虫过滤。
2026-10-29 18:25:53 +08:00
tamakyi 03cb5ddcf0 fix(ui): 提交防重复覆盖验证码路径、waline 按页加载、懒加载、safe-area、硬编码文案接字典
1) 防重复点击原来漏了最主要的一条路径
   footer 的全局 submit 监听会禁用按钮,但**程序化 form.submit() 不触发 submit 事件** ——
   走 reCAPTCHA / Turnstile 的流程是在回调里调 form.submit() 的,而默认配置
   captcha_type = recaptcha,也就是主提问流一直没有防重复。登录、注册、找回密码、
   投票、注销账号同样走这个回调。
   → 把「切提交中状态」抽成 tbBeginSubmit(form, trigger) 挂到 window,
     验证码回调先调它再 submit。7 个 data-callback="onSubmit" 的模板全部受益。
     顺带加了 data-tb-restore 的重复覆盖保护,避免 bfcache 复位后按钮卡在转圈。

2) waline.js / waline.css 从全站改按页加载
   只有 home.html 与 item.html 用评论区,但此前挂在 header 里 —— 登录、后台、投票、
   私聊每页都下载解析 waline.js(2.15 未压缩约 200KB)。实测 /login 已无引用。

3) 图片懒加载:explore 列表头像与题目配图加 loading="lazy"(首屏推荐位不加,避免影响 LCP)。

4) 移动端底栏:body 只预留固定 56px,而底栏 padding 含 safe-area-inset-bottom。
   带 Home Indicator 的 iPhone 上 inset 约 34px、底栏实际约 88px,最后约 32px
   (页脚备案、存活计时)被压在栏下,滚到底也看不到。改成 calc(56px + env(...))。

5) 硬编码中文接字典(en/ja 用户此前直接看到中文):
   history-template 的「查看回答 / 加载更多 / 加载中... / 无更多留言」、
   单题页「@xxx 以前回答过的问题」(字典里本来就有 ask.history)、
   投票页「(最多 N 项)」改为带参数的整句(原先前半走字典、后半手写中文)。
   新增 4 个 key × 4 语言。

实测:箱主语言设为 en 后箱主页显示 View answer ×2 / Load more / No more posts,中文零残留。
2026-10-29 16:28:53 +08:00
tamakyi 7802bb2078 fix(security): 修复安全审计的 P1/P2/P3 项,含全局复查新发现的两处越权与外跳
P1:
- 外链图片:markdown 渲染层只放行站内与自有 CDN 图片。配置里的
  image_bucket_cdn_host 常写成 host/path,统一归一化后再比对,避免把自家图一起剥掉
- 密码:页面/提问密码最小 6 位;密码比对挪到验证码之后;页面密码失败锁定 5 次/10 分钟
- 客户端 IP:CF-Connecting-IP / EO-Client-IP 只在 TCP 对端可信(回环或 trusted_proxies)
  时采信,公网对端带来的权威头一律忽略

P2/P3:
- 跳转参数收敛到 shared.SafeRedirectTarget:拒反斜杠与控制字符(url.Parse 会把 %0d%0a
  解成 CRLF,伪造 Location),最后再用 path.Clean 验一次前缀
- 注册不再暴露「邮箱已注册 / 已停用」:与全新注册共用同一个成功出口,只在日志留痕
- CSP:默认 enforce,白名单由配置派生,覆盖 umami(运行时注入)、Turnstile 与
  按 sentry_dsn 条件放行的 Sentry CDN
- /user 与 /_ 下发 no-store;补 X-XSS-Protection、Permissions-Policy,并把安全头前移到
  静态资源中间件之前(原先静态文件拿不到 nosniff/HSTS)
- 封禁事实落库 ban_records;429 统一带 Retry-After 与本地化文案
- token 比较改常量时间;探索页页码上限;JSON 响应显式 Content-Type

全局复查补的两处:
- 私聊 SendMessage 的 POST 缺 conv.OwnerID 校验,被拉黑者可换 domain 继续发消息
- 页面密码 next 与登录 to 同源的反斜杠外跳

验收:verify_p1_fixes 4/4、verify_round2 19/19、verify_page_next 6/6、
verify_chat_owner 3/3(含变异对照);单测与 go build ./... 全通过。
2026-10-27 12:37:40 +08:00
tamakyi c680671b7e feat(user/questions): 手机端时间跨年自动补年份,避免省略年份后产生歧义
DateCompact:与当前同一年的渲染「月-日 时:分」,跨年补回完整「年-月-日 时:分」。
只砍年份会让「去年今天」和「今年今天」显示成同一串,逐条补年份比按年分组便宜得多
(列表带游标分页 + 可切排序/筛选,组标题会跨页重复或错位)。
桌面端不受影响,仍显示 Y-m-d H:i:s。
测试覆盖年份边界(同年 1/1 与 12/31、跨年);预览数据里放了一条 2025 年的提问用于肉眼核对。
2026-10-23 21:04:50 +08:00
tamakyi 812b70cb4c fix(user/questions): 筛选标签改为「已读 是/否」,手机端条目标签行不再折行
- 筛选栏第 5 列标签「阅读状态」四字,在手机约 53px 的窄列里必然折成两行,
  改为「已读」,选项改为 全部/是/否(option value 仍是 read/unread,后端不变)。
  词条 qlist.readStatus 由 qlist.readFilter + qlist.filterYes/filterNo 取代,四语言同步。
- 条目行「时间 + 状态标签 + 标记/置顶」在手机上放不下:实测 360 宽可用 306px,
  完整时间单条就占 127px,2 标签 + 2 按钮需 343px。手机端时间改为「月-日 时:分」
  (tb-short/tb-text 复用状态标签的 640px 断点,桌面端仍显示完整年月日时分秒),
  同时把 .qlist-pill 的基础内边距 40/8 收到 34/6(桌面 @media 仍覆盖回 62/10)。
  结果:2 标签 + 2 按钮在 360 宽即单行;三标签(私密+待答+未读)在 ≤375 仍会折行。
2026-10-23 21:02:37 +08:00
tamakyi f37ff28a46 提问管理页面板补卡片底:换主题后内容不再裸铺在场景上
现象:只有 /user/questions 这一页的「卡片」像完全透明的,其它后台页正常。

根因不是主题,是模板本来就没有卡片容器:提问列表是
<div class=uk-margin-medium-bottom> + <hr> 直接铺在页面上,整个模板只有 1 处 .uk-card
(还是投票 tab 里的「我的投票」标题条)。默认主题底色是纯白,所以看不出来;
换成带整页插画的主题后,场景整片透上来,就成了「卡片透明」。

给 .qlist-panel 兜一层卡片底(--tb-surface + 圆角 + 阴影),三个 tab 共用;
面板内已有的一张 .uk-card-default 降成 --tb-surface-2 去阴影,避免卡片套卡片。
≤640px 收紧内边距。与主题无关,默认外观也会多这层卡(和其它后台页一致)。
2026-10-21 19:32:29 +08:00
tamakyi 1f29d3eb2e 修设置页主题色块被模板过滤器替换成 ZgotmplZ
html/template 对 style 属性做 CSS 过滤,linear-gradient(...) 未标记安全会被整体替换成
ZgotmplZ —— 线上设置页四个主题色块一直是灰底(既有问题,不是这次配色改动引入)。

- FuncMap 新增 safeCSS(string → template.CSS);值只来自 internal/theme.swatchOf,
  由两个已校验的色值拼成,不接受用户输入,所以这里放开是安全的
- user/profile.html 的色块改 {{ safeCSS .Swatch }}
- 新增 TestThemeSwatchRenders:渲染 user/profile,断言不含 ZgotmplZ 且含渐变串;
  已做变异验证(去掉 safeCSS 后该用例失败)
2026-10-21 18:53:10 +08:00
tamakyi 27856dd4a2 字号阶梯收尾 + 主色补到 WCAG AA(新增 --tb-on-primary)
一号事项(剩余令牌化):
- templates/user/profile.html 20 处、base.css 9 处 font-size 全部落到 --tb-fs-*
- base.css 导航四档(0.95/0.88/0.82/0.78)合并成「桌面 md → ≤1000 xs」;logo 三档统一 lg
- 有理由保留的:.tb-pin-flag / .md-body code 的 0.9em(相对父级)、验证码 1.4/1.2rem(等宽字形)、
  chat 关闭按钮 40px、password 页 3em、mail/*(邮件客户端不保证 var());行内 12px/0.75rem 等值顺手换掉

二号事项(主色对比度):
- base/default #1e87f0(3.42) → #0e6fd0(4.70);shiroko #4a7fc1(3.69) → #3b6dac(4.74);
  sakura #e5678f(2.80) → #ba3e66(4.70,饱和度压到 .5 保持柔和)。同色相只压亮度,目标 4.7 留余量
- 深色块 muted 之前只对 --tb-bg 算(4.7 以为过了),对压平后的卡片其实 4.31/4.17 → 提到 #a18790 / #7f8ea1
- 新增 --tb-on-primary(亮 #ffffff / 暗 #0b1016):深色模式主色偏亮,白字只有 2.2~3.1:1,
  用同一个令牌兼顾「链接色要深」和「按钮底要深」是矛盾的。.uk-button-primary 与
  .uk-label:not(语义色) 改吃它 → 深色按钮变成浅底深字(6.10 / 8.65 / 7.27)
- 顺手封掉几处主色泄漏:header 验证码 focus、profile .feat-day.is-on、explore 随机投票的 JS outline、
  vote-results 行内色,全部改成令牌(否则换主题时不跟着变)

生成器同步:paletteVars 导出 --tb-on-primary(白字/深字择优);?dump=palette 增加
onPrimaryCr / onPrimaryPickedBest / warnings —— 主色卡中间调时只警告不判失败;
配色面板给橙色提示。_scaffold 令牌 29 → 30。StaticVersion → 20261021c。

验证:TestSwatch 断言同步为 #ba3e66;8 种组合(base 亮暗 + 三主题亮暗)对比度全部 ≥4.5;
go test internal/{theme,templates,conf,i18n} 全绿;生成器 validate/fit/quality/palette/layers/fx/fxpipe 全过。
2026-10-21 18:39:08 +08:00
tamakyi 001306d940 模板样式令牌化:字号落到 5 档阶梯、圆角改走 --tb-radius
11 个模板里的硬编码字号/圆角换成令牌,为之后的深色与主题一致性打底。

字号(base.css 的 --tb-fs-xs/md/lg/xl/hero = 12/14/16/24/28):
- 0.70~0.88rem、0.72rem 等五六档零碎值 → --tb-fs-xs(标签/meta) / --tb-fs-md(正文、列表、按钮)
- 0.95~1rem → --tb-fs-lg;1.5rem 大数字 → --tb-fs-xl;1.75rem 首页主标题 → --tb-fs-hero
- 以下几处**故意改变了观感**(不是等价替换,先记下来免得以后当 regression 查):
  explore 的 .explore-name 1.2rem → 1rem;.hot-vote-title 0.98 → 0.875;
  首页 .tb-stat-num 手机端 1.25 → 1rem;.hot-vote-action 0.82 → 0.875(变大)。

圆角:
- 8/9/10px → --tb-radius-sm(8) / --tb-radius(12,对话框与气泡);3/4/5/6px → --tb-radius-xs(6)
- 头像 50%、胶囊 999px 保留原样(语义值,不是视觉随意值)

其它:header 验证码输入框边框 #ccc → var(--tb-border);chat 气泡与图片的圆角走内联 var()。

仍未令牌化(留给下一轮):templates/user/profile.html 还有 16 处 font-size、base.css 自身 11 处
(0.78/0.82/0.88/0.95/1.1),邮件模板 mail/* 保持 px 不动(邮件客户端不保证 var())。
2026-10-21 18:28:26 +08:00
tamakyi 6c014b3ad2 修 test01:窄屏装饰全藏导致素材消失、主题名显示成 i18n key
- 生成器:勾「窄屏隐藏全部装饰层」时给橙色警告(几层会消失)、图层行标「窄屏隐藏」、加层与导出结果再提醒;「窄屏」下拉在被全藏覆盖时标注不生效
- 新增 ?narrow=hide 复现该状态(这是「素材在手机上看不到」的头号来源:@media 把 10 槽全写 none,桌面完全正常)
- 主题名:新增 theme.DisplayName + 模板函数 themeName,i18n 未登记 theme.name.<slug> 时回退 slug;原来模板直接调用 i18n.T,缺词条就显示 "theme.name.test01"
- test01.css 删掉 @media 全 none 段:装饰是 fit 单位,浏览器会自己按视口缩,不需要这段
- StaticVersion → 20261020g(主题 css 内容变了,必须换缓存键,否则浏览器还会用旧文件)
2026-10-20 18:23:21 +08:00
tamakyi e9fba8b9ed theme v1 2026-10-19 15:23:11 +08:00
tamakyi 86dc8384bc 全局美化 v5 2026-10-19 12:44:32 +08:00
tamakyi d7c1782bb6 全局美化 v4 2026-10-19 12:07:21 +08:00
tamakyi 5c7707ec0d 全局美化 v3 2026-10-19 11:43:24 +08:00
tamakyi d47329d505 全局美化 v1 2026-10-19 11:08:30 +08:00
tamakyi 12a37e726f fix v14 2026-10-19 02:25:19 +08:00
tamakyi bd613a6f5a fix v12 2026-10-19 01:11:28 +08:00
tamakyi efd5fce9a7 restrict v10 2026-10-19 00:07:22 +08:00
tamakyi 7e0e1cba49 restrict v9 2026-10-18 23:48:57 +08:00
tamakyi 7a6417c33d restrict v8 2026-10-18 23:32:38 +08:00
tamakyi 317c96ca17 restrict v7 2026-10-18 23:04:50 +08:00
tamakyi bd81398a06 restrict v6 2026-10-18 22:18:21 +08:00
tamakyi 6bd90779f8 restrict v1 2026-10-18 21:14:59 +08:00
tamakyi 5ee2dc1540 fix login error 2026-09-15 13:10:57 +08:00
tamakyi 0e0c16f786 fix deactivate 2026-09-15 12:10:42 +08:00
tamakyi e683e798aa fix typo error 2026-09-10 19:27:21 +08:00
tamakyi 8e2325eda5 aliyun sensor server 2026-09-10 18:04:19 +08:00
tamakyi e3e59e7c69 chore: baseline snapshot before moderation-strictness feature
Repo .git was damaged (refs/ missing, index truncated, object store near-empty).
Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak,
then full worktree committed as a rollback baseline.
2026-09-10 10:56:27 +08:00