Files
jellybit/internal/httpapi/render.go
T
av 2971a520c7 веб-ui: знак «jb» стал единым для шапки, иконки вкладки и превью ссылки
- логотип сведён в web/static/logo.svg (буквы геометрией, оптическая
  центровка); прежний favicon с «play» убран
- <head> шести страниц свёрнут в партиал head с og-мета; адреса абсолютные,
  база берётся из запроса, хост из недоверенного заголовка проверяется
- растровые ассеты og-cover.jpg и apple-touch-icon.png собирает `task brand`
  из исходников web/*.html
2026-09-02 10:55:45 +03:00

94 lines
3.1 KiB
Go

package httpapi
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"io/fs"
"net/http"
"git.vakhrushev.me/av/jellybit/web"
)
// assetVersion — короткий хеш изменяемых ассетов (css/js, знак и карточка
// превью) для cache-busting. Шрифты и вендор адресуются по неизменному имени,
// их версионировать не нужно.
func assetVersion() (string, error) {
h := sha256.New()
for _, p := range []string{
"static/css/jellybit.css", "static/js/app.js",
"static/logo.svg", "static/og-cover.jpg",
} {
b, err := web.FS.ReadFile(p)
if err != nil {
return "", err
}
_, _ = h.Write(b)
}
return hex.EncodeToString(h.Sum(nil))[:12], nil
}
// staticHandler отдаёт встроенную статику с длинным иммутабельным кэшем —
// URL версионируются через ?v=<assetVersion> в шаблонах, поэтому свежий
// деплой не отдаёт устаревший файл.
func staticHandler(fsys fs.FS) http.Handler {
fileServer := http.FileServer(http.FS(fsys))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
fileServer.ServeHTTP(w, r)
})
}
// badgeLabel — человекочитаемая подпись состояния для бейджа (см. handoff п.4).
// CSS-класс берётся отдельно как st-<state>; неизвестное состояние показываем
// как есть, чтобы не терять его в UI.
func badgeLabel(state string) string {
switch state {
case "catched":
return "🎣 принято, добавляется"
case "downloading":
return "⬇ качается"
case "completed":
return "⬇ скачано"
case "recognizing":
return "🧠 распознаётся"
case "linking":
return "🔗 раскладка"
case "review":
return "🟡 на ревью"
case "deferred":
return "🕗 отложено"
case "done":
return "✅ готово"
case "stuck":
return "⏳ застряло"
case "target_missing":
return "⚠ нет цели"
case "failed":
return "⛔ ошибка"
case "orphaned":
return "⚠ потеряно"
case "cancelled":
return "✖ отменено"
case "reverted":
return "↩ откат"
case "deleted":
return "🗑 удалено"
default:
return state
}
}
// render отрисовывает именованный шаблон в буфер и только затем пишет ответ —
// при ошибке шаблона клиент не получит «полустраницу».
func (s *server) render(w http.ResponseWriter, name string, data any) {
var buf bytes.Buffer
if err := s.tmpl.ExecuteTemplate(&buf, name, data); err != nil {
s.deps.Logger.Error("render", "template", name, "error", err)
http.Error(w, "внутренняя ошибка", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = buf.WriteTo(w)
}