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"} { 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= в шаблонах, поэтому свежий // деплой не отдаёт устаревший файл. 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-; неизвестное состояние показываем // как есть, чтобы не терять его в 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) }