- логотип сведён в web/static/logo.svg (буквы геометрией, оптическая центровка); прежний favicon с «play» убран - <head> шести страниц свёрнут в партиал head с og-мета; адреса абсолютные, база берётся из запроса, хост из недоверенного заголовка проверяется - растровые ассеты og-cover.jpg и apple-touch-icon.png собирает `task brand` из исходников web/*.html
94 lines
3.1 KiB
Go
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)
|
|
}
|