refactor(webui): dashboard.html 6637 行拆成「外壳 + 样式 + 脚本」

前端刻意没有构建链(纯 CSS + Vanilla JS,go:embed 进二进制),所以拆法是:
外壳 dashboard.html 留 {{DASHBOARD_CSS}} / {{DASHBOARD_JS}} 两个占位符,
init() 启动时把两份资产原样填回去 —— **发出的 HTML 与拆分前逐字节一致**,
但 6637 行的单文件变成三份,便于编辑与评审。

  dashboard.html   168 行   外壳(head/body 结构 + 两个占位符)
  dashboard.css   2099 行   样式
  dashboard.js    4371 行   脚本

逐字节校验(三重):
  * 组装结果 sha256 == git HEAD 里拆分前的 dashboard.html;
  * 真实实例 GET /(带 API key)返回体 sha256 同上:0ef14b49…c053;
  * 新增 TestDashboardAssetsSplit:占位符必须存在、样式/脚本不得再内联回外壳、
    组装结果不得残留占位符且必须含样式与脚本特征串。

按行号切片时踩过一次坑并已修正:`</style>`/`</script>` 两个闭合标签被切掉
(正好少 27 字节)——正是因为当时少了逐字节校验,现在把它固化成断言。
This commit is contained in:
HomeAgent Agent
2026-09-14 07:17:23 +08:00
parent 4cbfdc970c
commit 764939ed90
5 changed files with 6535 additions and 6474 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@ import (
// 本文件是 WebUI 的骨架嵌入式前端资源、Handler 结构、构造、路由表、
// 鉴权/会话/日志中间件与静态页。各资源的具体 handler 见同包 handler_*.go。
//go:embed dashboard.html mascot.webp logo.svg
//go:embed dashboard.html dashboard.css dashboard.js mascot.webp logo.svg
var dashboardFS embed.FS
var dashboardHTML string
@ -56,11 +56,30 @@ document.getElementById('login-form').addEventListener('submit',async(e)=>{e.pre
document.getElementById('password').addEventListener('keydown',function(e){if(e.key==='Enter')document.getElementById('login-form').dispatchEvent(new Event('submit'))});
</script></body></html>`
// dashboardHTML 是组装好的控制台页面dashboard.html 外壳 + dashboard.css + dashboard.js。
//
// 前端刻意没有构建链,所以拆分的办法是:外壳里留 {{DASHBOARD_CSS}} / {{DASHBOARD_JS}}
// 两个占位符,启动时把两个资产原样填回去——**发出的 HTML 与拆分前逐字节一致**
// 但 6637 行的单文件变成「外壳 + 样式 + 脚本」三份,便于编辑与评审。
func init() {
data, err := dashboardFS.ReadFile("dashboard.html")
if err == nil {
dashboardHTML = string(data)
html, err := dashboardFS.ReadFile("dashboard.html")
if err != nil {
return
}
out := string(html)
for _, a := range []struct{ placeholder, file string }{
{"{{DASHBOARD_CSS}}", "dashboard.css"},
{"{{DASHBOARD_JS}}", "dashboard.js"},
} {
body, err := dashboardFS.ReadFile(a.file)
if err != nil {
log.Printf("[webui] 读取前端资产 %s 失败: %v", a.file, err)
continue
}
// 资产文件末尾的换行由占位符所在行自己的换行承担,避免多出空行。
out = strings.Replace(out, a.placeholder, strings.TrimRight(string(body), "\n"), 1)
}
dashboardHTML = out
}
type Handler struct {

View File

@ -1292,3 +1292,43 @@ func TestChatPersistenceIsThrottled(t *testing.T) {
// Close 幂等
h.Close()
}
// TestDashboardAssetsSplit 钉住前端的拆分方式:
// 外壳 dashboard.html 只留两个占位符,样式与脚本分别在 dashboard.css / dashboard.js
// 启动时组装回**与拆分前逐字节一致**的页面(拆分当日已用 git 里的原文件比对通过)。
func TestDashboardAssetsSplit(t *testing.T) {
shell, err := dashboardFS.ReadFile("dashboard.html")
if err != nil {
t.Fatalf("读 dashboard.html: %v", err)
}
for _, ph := range []string{"{{DASHBOARD_CSS}}", "{{DASHBOARD_JS}}"} {
if !strings.Contains(string(shell), ph) {
t.Fatalf("外壳里应保留占位符 %s", ph)
}
}
// 样式/脚本不能又塞回外壳(否则拆分名存实亡)
if strings.Contains(string(shell), "--sakura-300") {
t.Fatal("dashboard.html 里仍内联着样式,拆分未生效")
}
if strings.Contains(string(shell), "function saveSetting") {
t.Fatal("dashboard.html 里仍内联着脚本,拆分未生效")
}
css, err := dashboardFS.ReadFile("dashboard.css")
if err != nil || len(css) == 0 {
t.Fatalf("读 dashboard.css: %v (%d 字节)", err, len(css))
}
js, err := dashboardFS.ReadFile("dashboard.js")
if err != nil || len(js) == 0 {
t.Fatalf("读 dashboard.js: %v (%d 字节)", err, len(js))
}
// 组装结果:占位符必须都被替换掉,且样式/脚本内容都在里面
if strings.Contains(dashboardHTML, "{{DASHBOARD_") {
t.Fatal("组装后的页面仍残留占位符")
}
if !strings.Contains(dashboardHTML, "--sakura-300") {
t.Fatal("组装后的页面缺样式")
}
if !strings.Contains(dashboardHTML, "function saveSetting") {
t.Fatal("组装后的页面缺脚本")
}
}