22 KiB
2026-10-22
new-sakura 主题:纯 CSS 樱花
用户要求「用编辑器生成 new-sakura,CSS 做樱花、每页位置不同、上传到已配好的 S3」。
- 结论:生成器做不了这个 —— 它的装饰层只支持图片素材(导出打 zip / 传对象存储), 没有内联 SVG 图层。所以改成手写主题 CSS(结构完全遵循生成器输出:令牌 + 10 槽 deco + scope 覆盖),生成器只用来做视觉验证。
- 实现:
dev/theme-tools/gen_new_sakura.py生成static/css/themes/new-sakura.css。 花/瓣/枝/底纹都是内联 SVG 的 data URI(quote(svq, safe="")全编码,避免 URI 里的 逗号被TestDecoSlotCount当成分层符)。CSS 50KB(gzip ≈12KB)。 - 六个 scope 各一套
--tb-deco位置;窄屏(≤640px)装饰全关、底纹缩到 200px; 深色换亮粉 + 关底纹(浅粉压深底=噪点)。 - 调参踩到的:①花枝
auto 390px放右上角时可见部分只剩一团花 → 缩到 300px 并内移; ②枝上 5 朵 scale≈1 的花是一串糖葫芦 → 改成 8 朵 0.4~0.62; ③底纹 260px 双花双瓣读成「包装纸」→ 改成 300px 一朵花 + 两片瓣、各自错位。 - 对比度:primary 候选
#c14a72只有 4.39:1,压深到#b8436a(4.87);muted#77626a(5.27); 深色#ff93b4(8.17)/#ab9099(5.82)。 - i18n 四份词典加
theme.name.new-sakura(插在 shiroko 之后,gofmt_keep_crlf 后每份只 +1 行);conf/app.sample.ini的 available 登记。 - ⚠️ 本机 Chrome/Edge 的 localStorage 里没有生成器的 S3 配置记录,项目也没有
conf/app.ini→ 「已预先配置好的 S3」在本机取不到,上传这一步需要用户给凭据。 纯 CSS 主题本身零素材,严格来说 S3 无事可做(已向用户确认)。
R2 自定义域可用 + 素材包一键上传(接上一条)
r2.337845818.best实测公开可读(7 个对象全部 200、Content-Type 正确);桶的 S3 endpoint 仍 403。- 樱花位图从
gen_new_sakura.py的同一份 SVG 导出(export_assets.py直接 import 它,不另画),--upload一次传 CSS + 5 张 webp + 生成的 INSTALL.txt(域名/体积写实数)。 serve.upload_files支持逐文件cacheControl:原来硬编码 immutable,而INSTALL.txt/ 镜像 CSS 的键不带版本 → 改了看不到。现在位图 1 天、CSS 5 分钟、README 60 秒。- ⚠️ CF 边缘缓存不会因为重传而失效:第一版以 immutable 传过,
cf-cache-status: HIT、Age>800, 重传后拿到的仍是旧副本(带?cb=才是新内容)。清缓存只能去控制台按 URL 清。 - ⚠️ gzip 纠错:服务端
internal/route/gzip.go会压 CSS,内联版 51KB 实际传输 3.9KB, 位图版 2.7KB + 5 张图 48.9KB ≈ 51.6KB。所以"换位图省 CSS 体积"是错的 —— 位图唯一的理由是素材要给非 CSS 场景复用。文档与 INSTALL.txt 都已改。 image_bucket_cdn_host沿用 app.ini 同名键(只写主机名),export_assets.py据此拼公开地址。
线上 521 排障(box.shiroko.one 打不开)
- 现象:
https://box.shiroko.one全路径 521,同机 IP+端口正常。 - 实测链路:
box.shiroko.one→ CNAMEcloudflare.182682.xyz(第三方 CF SaaS 接入/优选, 上游source.cfnode.eu.org),不是<UUID>.cfargotunnel.com。DNS/TLS 均正常(tls=0), 响应体error code: 521,5 次请求全 LAX,0.6s 快速失败(RST 特征,非超时)。 - 对照实验有价值:
box.tama.guru走同一个 CNAME 目标却正常 301 → 中转 zone 边缘健康; 它不回源,而 box.shiroko.one 要回源才 521 ⇒ 断点在「CF → 源站」这一段。 shiroko.one/www/tama.guruapex 都 CNAMEnavi-ew2.pages.dev(CF Pages 导航站),与本站无关, 别拿它们的 200 当证据。- 候选根因(待服务器侧确认):① cloudflared 断开(严格说 tunnel 无 connector 多为 502/1033);
②
scripts/edgeone-origin-whitelist.sh的REJECT tcp-reset拦掉 CF 直连回源——脚本注释里 「521」本就是白名单漏放的既有症状;③ 回源 IP 过期(服务器 IP 变动 / 第三方面板未更新)。 - 注意矛盾点:用户本地能连 IP+端口 ⇒ 白名单大概率没生效或不覆盖该端口 ⇒ 更偏向 ③/tunnel 侧。
根因已确认(服务器实测)
systemctl is-active cloudflared= inactive,pgrep无进程 → Tunnel 断。 journald 最后一条 2026-10-21T21:42:22Z(CST 10-22 05:42),此后无日志 ⇒ 停了约 7 小时。- 死前日志模式:
accept stream listener encountered a failure while serving+failed to accept QUIC stream: timeout: no recent network activity+datagram handler error="context canceled"+Retrying connection in up to 1s,connIndex 2/3 反复 → UDP 7844 QUIC 长时间不稳(美西 23.94.211.138)。inactive 而非 failed ⇒ 是被正常 stop 的, 不是崩溃,与用户「昨晚升级 nginx 到 1.30.x」的时间点吻合(升级流程连带停服)。 - 应用侧全好:
*:5877监听(tamabox pid),loopback 200。 iptables -S INPUT只有-i eth0 --dport 8118/5877 -j DROP,EO_IN / ipset EO_ORIGIN 都不存在 ⇒ EdgeOne 白名单脚本当前未生效。且 DROP 限定-i eth0,网卡名若不叫 eth0 则规则空转。- 关键推论:CF 一定走 tunnel 而非直连 5877(否则 DROP 空转应 200 而非 521)⇒ 521 完全由 tunnel 断解释。
续:start 之后仍 521
- 用户确认:5877 只允许 Tailscale(100.84.113.84:5877)访问,公网靠 iptables DROP 封掉。 ⇒ 排除「CF 直连公网 5877」这条路径,CF 只能经 tunnel 回源。
systemctl start cloudflared→active,sleep 5后 curl 域名仍 521。- 待分辨:① connector 尚未完成边缘注册(sleep 5 偏短);② 本机 tunnel 与 CF/SaaS 侧
box.shiroko.one绑定的 origin(应为<uuid>.cfargotunnel.com)不是同一个; ③ 该 hostname 的 Public Hostname / ingress 未指向http://127.0.0.1:5877。 - 决定性隔离实验:临时 quick tunnel(
cloudflared tunnel --url http://127.0.0.1:5877) 拿 trycloudflare 域名直测 —— 200 则应用+出站正常,问题在 CF 侧绑定。
journald 取证(关键)
protocol=quic,location=la01。从 10-21 13:53:30 起就是死循环:INF Registered tunnel connection→ 约 3s 后ERR failed to accept QUIC stream: timeout: no recent network activity+datagram handler error="context canceled"+WRN failed to serve tunnel connection error="accept stream listener encountered a failure while serving"→INF Retrying connection in up to 1s→ 再注册。connIndex 0/1/2 全部如此, 每 ~3 分钟一轮,一直持续到 10-21 21:42:40。- ⇒ UDP 7844 的 QUIC 在这台机器上基本不可用(RackNerd 美西 → CF la01),这是历史顽疾,
不是本次新故障。根治手段 =
TUNNEL_TRANSPORT_PROTOCOL=http2(走 TCP 443)。 - 🔴 但 12:56
systemctl start之后 journal 里一条新日志都没有(最后仍是 10-21 21:42:40, PID 626)⇒ 新进程疑似启动即退出/token 无效,is-active=active可能是重启窗口里的瞬时值。 需systemctl status看 Restart 计数与 ExecMainStatus。 - 旁证:EdgeOne 那条备用通道同样不通(18080 无监听、EO_IN/ipset 均不存在)。
结论定案
- 重大纠正:cloudflared 从未死过。
systemctl status显示active (running) since 2026-10-20 05:32:44, Main PID 626 存活 2 天+。前面「21:42 后无日志」不是进程退出,而是 QUIC 连接最后注册成功后没再报错 (只有 24h 一次的版本提醒)。所以「tunnel 断导致 521」这个假设是错的。 - 改
TUNNEL_TRANSPORT_PROTOCOL=http2后:4 个 connector 全部Registered tunnel connection ... protocol=http2,SUMMARY: Environment is healthy. Cloudflared will use 'http2' as primary protocol,零错误 ⇒ tunnel 侧完全健康。QUIC 抖动问题也顺手解决了(该 drop-in 值得长期保留)。 - 但站点仍 521 ⇒ 问题 100% 在 CF 侧 origin/路由,与本机 tunnel 无关。
- 公网实测
23.94.211.138:5877 / 18080 / 80 / 443 全部 timeout(不止 5877)。 ⇒ 若第三方 SaaS 的 origin 填的是「公网 IP:5877」,CF 同样连不上 → 521。 这解释了「突然」:用户近期把 5877 收敛为仅 Tailscale(100.84.113.84)可达。 - 本机 tunnel 身份(从 token 解出,注意 token 已明文暴露需轮换):
tunnel id
ba34e35d-e517-498a-b2cd-7275c22e354e,account tag0c99c40b475a65b393abaca3ca91731b。 - 修复方向:①(推荐)第三方面板把 origin 改成
ba34e35d-...cfargotunnel.com,并在 Zero Trust → Tunnels → 该 tunnel 的 Public Hostnames 补box.shiroko.one → http://127.0.0.1:5877; ②(不推荐)放行 CF 回源网段到 5877,等于重新暴露源站。 - ⚠️ 安全 P0:tunnel token 在对话截图里明文出现,等同凭据泄露 → 必须 Refresh token 后重装服务。
真凶:SaaS origin 指向被封的公网
- SaaS 自定义主机名
box.shiroko.one的「源服务器」=box-direct.337845818.best。 - 实测该 origin 域名 解析到 Cloudflare 自己的 IP(104.21.47.52 / 172.67.144.141,
Server: cloudflare,CF-RAY)⇒ 它是橙云(proxied)记录,不是灰云 A 记录。http://301(Always Use HTTPS)、https://521 —— 它自己也回源失败。 - 源站
23.94.211.138(colocrossing,RackNerd 上游):22 / 80 / 443 / 5877 / 18080 全部 timeout。不只应用端口,连 SSH 都不通 ⇒ 该机对公网几乎无入站(仅白名单/Tailscale)。 - 结论链:CF 回源
box-direct→ 该记录橙云 → 真实源站端口被封 → 521。 所有共用-directorigin 的站点同时 521 ⇒ 用户说的「cloudflared 构建的网站全部 521」 与 cloudflared 无关,是公网入站收敛这一个动作把整批站点打掉的。nginx 升级纯属时间巧合。 - 修复(保持「只允许组网」的安全目标):① Zero Trust → 该 tunnel → Public Hostnames 加
box.shiroko.one → http://127.0.0.1:5877;② SaaS 面板把每个自定义主机名的「源服务器」 从xxx-direct.337845818.best改为ba34e35d-e517-498a-b2cd-7275c22e354e.cfargotunnel.com。 注意不能把-direct改成「灰云 CNAME 到 cfargotunnel」——tunnel 路由要求橙云,SaaS origin 又 要求非橙云,两者冲突,必须直接在 origin 字段填 cfargotunnel。
⚠️ 纠正(官方文档)
- 上一条的后半段判断错了。CF for SaaS 文档明确:custom origin 必须是该账号下
proxied(橙云)的 A / AAAA / CNAME 记录,不能用 IP。
⇒
box-direct.337845818.best是橙云,格式上完全正确,不是配置错误。 用 tunnel 的正确形态就是「橙云 CNAME →<uuid>.cfargotunnel.com」,所以那条路径依然成立, 只是理由不是「橙云错了」。 - 真正断点仍是:
23.94.211.138的 443 从公网不可达(实测 timeout)。 但用户贴的iptables -S INPUT里只有 8118 / 5877 两条 DROP,没有 443 ⇒ 对 443 的封锁来自 iptables 之外(云厂商安全组 / 上游 / 其他链),或者 nginx 压根没监听 443。 - 我上一轮的探测命令
grep ':(80|5877|18080)'漏了 443,这是取证缺口,需要补ss -tlnp | grep -E ':(443|80|5877)'+systemctl is-active nginx。 - 注意:SaaS 回源默认 Host = 原始自定义主机名(box.shiroko.one),而 tunnel 的 Public Hostname 只能从账号内 zone 选(shiroko.one 不在 CF)⇒ 走 tunnel 时必须用 Origin Rule / 源站主机头覆盖, 否则 tunnel 会 404。
最终根因(用户自查确认):nginx 停了 11 小时
- 时间线:
10-22 00:12:43 Started(升级后)→02:31:10 Stopping...→02:31:15 Stopping timed out. Terminating.→Failed with result 'timeout'→Stopped→ 中间 11 小时无任何记录 →13:34:42用户手动systemctl start才起来。 - ⇒ 全站 521 的直接原因就是 nginx 不在 443 监听;CF 回源
box-direct(橙云 A → 23.94.211.138:443) 拿不到东西。cloudflared/tunnel 全程健康且从未参与这条链路。 - 重启后实测:
box.shiroko.one连续 3 次 200,box-direct200,源站 443 OPEN。 - 教训(我的误判链,值得记住):排障时先确认「443 上跑的是什么、进程在不在」(
ss -tlnp+systemctl is-active nginx),再往 DNS/CDN/tunnel 方向挖。这次因为 CNAME 指向第三方 SaaS- cloudflared 恰好健康,被带了很远。
- 遗留技术债:
①
listen 443 ssl http2在 1.25.1+ 已废弃 → 改listen 443 ssl;+http2 on;; ②Failed to parse PID from file /run/nginx.pid: Invalid argument—— 大概率是上次 SIGKILL 残留的 pid 文件,配合TimeoutStopSec过短(5s 就 Terminating)导致 stop 卡死; ③ 全站无可用性监控(11 小时空窗没人知道)→ 建议外部 uptime + cron watchdog*/5 * * * * systemctl is-active --quiet nginx || systemctl start nginx。
6) 窄屏覆盖被作用域块压死(真 bug,已修)
- 现象:
@media (max-width:640px)里把--tb-deco全写 none,手机上却照样是桌面装饰层。 - 根因:媒体查询不提升特异性。作用域块
html[data-theme-style][data-page-scope="box"]是 (0,2,1),媒体里写的html[data-theme-style]只有 (0,1,1) → 压不住;深色作用域块 (0,3,1) 更难。 - 修法:媒体查询里按「作用域通配
[data-page-scope](0,2,1)」+「深色基础(0,2,1)」+ 「深色+作用域(0,3,1)」各写一条,靠文件更晚取胜。shiroko 的 900px 块只对浅色有效, 但它的深色块本来就是 none,所以不算坏。 - 守门:新增
internal/theme/narrow_test.go::TestNarrowMediaOverridesScopedDeco——外层每处非 none 的--tb-deco,媒体查询里必须有「同属性名、属性数不少于它」的选择器; 变异验证过(删掉深色基础那条立刻 FAIL)。 - 手机端最终决定不放装饰层:试过顶栏与卡片之间那条 35px 带子,box 页 OK、question/admin 页卡片从 y=110 起就切半个 → 变成黏在卡片边的粉色碎块。改用加强版底纹(透明度 ×1.9、格子 180px)。
- 埋点:
gofmt -l internal/theme会报slots_test.go(它是 CRLF),新增文件是 LF,别顺手格式化它。
站内公告功能(配置驱动)
-
新增
[announcement]段(content/start/end),改完重启生效;判定全在服务端按 time.Now()。 -
改动:conf/announcement.go(解析+Active+单测)、conf.Init MapTo、keynorm sectionKeySets 登记(漏了会误报未知段)、 template.FuncMap 新增
announcement(返回安全 HTML,空串 falsy 配{{ with }})、 internal/template/announcement.go(切段转义→URL 包链接,不能「先转义再正则」否则 " 被吞进 URL)、 header.html 导航下方插uk-container-xsmall容器包.tb-announce、base.css 样式(StaticVersion → 20261022a)。 -
时间格式:
2006-01-02 15:04/ 带秒 / 纯日期 / RFC3339;裸时间按服务器本地时区(容器是 UTC),启动 INFO 打印解析结果。 -
时间写错只 WARN 关公告不阻断启动(与未知键口径一致);窗口左闭右开。
-
预览验证:TEST_PREVIEW=1 + 临时 test 设 conf.Announcement 渲染 question/list,chrome --headless=old 截图 OK(亮/暗都看过)。
-
公告间距修复:
.tb-announce自身的 margin-bottom 会被 UIkit 的.uk-container > :last-child { margin-bottom: 0 }清零 → 间距必须挂在容器类.tb-announce-wrap { margin-bottom: 20px }上。这是容器内末元素贴边的通用坑, 以后在.uk-container里加末位区块都绕这条。 -
环境坑:沙箱 go 是 1.25.5,go.mod 要 1.27.3,GOTOOLCHAIN=auto 需下载工具链模块, 而 GOSUMDB=off 会卡住校验 → 下载工具链那次要放开 GOSUMDB(goproxy.cn 会代理 sumdb)。
7) 顶栏:实色 → 半透明磨砂(用户:不能全透明,以后别再来一次)
- 线上
https://box.shiroko.one/_/tamakyi实测:--tb-navbar=#fdf6f8实色,顶栏带(y=20..80) 把右上花枝齐刷刷切一段;--tb-navbar默认是--tb-surface,彩色主题不覆盖就成一条近白横带。 - 第一版改成
transparent被用户否掉:花枝完整了但顶栏"消失",导航没依托、文字压在花纹上。 - 最终:
rgba(253,246,248,0.72)/rgba(34,26,30,0.72)+backdrop-filter: blur(12px)(模糊只能写在规则里,令牌只是颜色)。0.72 有依据:文字#6d555d纯底 6.34:1, 最坏(深花瓣压顶栏后面)压平后 5.08:1。 - 花枝
top从 -16px 下移到 26px:顶栏透明后 700~760px 宽度下枝头会压到「登录」(截图确认)。 - 守门:
internal/theme/navbar_test.go::TestNavbarNotFullyTransparent拦transparent/rgba(...,0)/#rrggbb00,变异验证过。约定同步写进static/css/themes/_scaffold.css的令牌注释与topics/theme.md。 - 手机底栏
#tb-mobile-utils在 base.css 写死rgba(255,255,255,.92),彩色主题要自己调(本例改同色系 0.92)。 - ⚠️
StaticVersion→20261022c:主题 CSS 改了必须提版本号,否则浏览器拿着旧 css, 现象就是「改了但线上没变」。 - ⚠️ 跑 go 命令时别加
GOSUMDB=off:本机 go 1.25.5,go.mod 要 1.27.3,GOTOOLCHAIN=auto要校验 toolchain 模块,关掉 sumdb 会直接报 "verifying module"。 只设GOPROXY=https://goproxy.cn,direct GOFLAGS=-mod=mod即可(toolchain 已缓存)。
8) 生成器新增「顶栏」面板:实色 / 半透明(磨砂)/ 全透明
- 状态
S.navbar = {mode, alpha, blur}(默认 glass 0.72 / 12px),基色仍取「配色 → 顶栏」,navbarValue(p)统一算:solid →rgb()(先 flatten 压平旧工程里的 rgba)、glass →rgba()、 clear →transparent。模糊单独写.uk-navbar-container:not(.uk-navbar-transparent)规则, 一条同时管深浅(选择器不带 data-theme)。 - 全透明只给预览:
validate()直接拦(导出报错),与TestNavbarNotFullyTransparent同一条规矩。 面板上写明原因 —— 不然下一轮又会有人问「为什么不能导出」。 - 对比度读数按「压平后的顶栏」算(半透明时是顶栏压在底色上的结果),低于 4.5 变橙。
- 旧工程迁移:配色里塞过
rgba(...,0.52)的,按 alpha 反推档位(1→solid,0→clear,其余→glass)。 - 自检
?dump=navbar10 项;Playwright 端到端 17 项(含滑杆跟手、刷新后保持、迁移三例)。 - ⚠️
?fresh=1每次加载都会清草稿 —— 测「刷新后配置还在」必须导航到不带 fresh 的地址。 - ⚠️ Playwright 的
fill()对input[type=range]报 Malformed value,要 evaluate 设 value + dispatch input。 - ⚠️ 滑杆步长别用 0.02:0.55 会被吸附成 0.54/0.56,用户以为坏了。改 0.01。
单题页「下一条未读」(箱主收件箱式导航)
- 新增
db.Questions.NextUnread(ctx, userID, excludeID):未读 =is_read=false AND answer='',created_at ASC取第一条,无则返回(nil, nil)。正序=先来先处理,反复取即可清空积压。 POST /_/{domain}/{qid}/next-unread(question.NextUnread):先 SetRead 再跳。 只跳不标记会「打转」—— 当前这条仍是未读,下一次取最早未读又把它选回来(2 条未读时最明显)。PublishAnswer支持同表单第二个提交按钮name="next" value="1"(「回答并下一条未读」)。- 按钮位置:回答行
[回答][回答并下一条未读](箱主+有下一条时);页脚与「返回提问箱」同一行 放下一条未读 ›(POST 表单 +uk-button-link,因为要改状态)。 adminBackQuery(ctx)抽出(原 BackTo 逻辑),NextQuery进模板 → 答案表单/next-unread 表单的 action 都带上?from=admin&sort=...,跳转后续「返回」仍回带筛选的后台列表。 flamegoctx.Query只读 URL query,所以 POST 必须把参数写在 action 里(不是隐藏域)。- 预览:
templates/zz_preview_test.go新增writeQuestionOwnerPreviews(未回答/已回答 × 深浅)。 ⚠️ 预览 HTML 的base.css是/static/...绝对路径,file:// 打开不含主题变量(暗色看着像亮色); 要验暗色必须python -m http.server起本地服务走 http。移动端预览本身有横向溢出(waline 表情行所致),非本次改动。 - 移动端排版实测(关键数据):390px 视口下
.tb-ans-row只有 300px(360px 视口 270px), 而「回答 90 + 回答并下一条未读 174 + 标记为已读 132 + 间距 20」= 416px,收窄内边距也不够(320px)。 → 手机上一行放不下三个按钮,改成:第 1 行保留原「回答 … 标记为已读」,第 2 行「回答并下一条未读」整行宽。 实现:@media(max-width:640px){ .tb-ans-actions{display:contents} .tb-ans-state{order:2;margin-left:auto} .tb-ans-actions .uk-button{order:1} .tb-ans-actions .uk-button.tb-ans-next{order:3;flex:1 1 100%} }⚠️.tb-ans-actions .uk-button(0,2,0)特异性高于.tb-ans-next(0,1,0),必须再写一条同级选择器盖 order。 - 页脚两个次级操作之间加
.tb-foot-sep(·,浅色 #b0b6bd / 暗色 #5c6674),原来两个蓝色链接几乎连成一片。 - Chrome headless 在 Windows 上
--window-size< 500 无效(innerWidth 恒 500)——要测真·移动端布局, 用「父页面 + 宽 390 的 iframe」再取contentDocument量尺寸/截图,媒体查询才会按 iframe 宽度生效。 - 文案定为「回答并进入下一条未读」(en: Answer & open next unread / ja: 回答して次の未読を開く)。 实测宽度:该按钮 202px;661px 视口(桌面态最窄)行宽 526 ≥ 90+202+132+20=444,仍单行; 640px 断点两侧切换干净,无需再调。