Files
flatrender/services/render/internal/handlers/fonts.go
T
soroush.asadi 7f2f65dd8a
Build backend images / build content-svc (push) Failing after 53s
Build backend images / build file-svc (push) Failing after 47s
Build backend images / build gateway (push) Failing after 52s
Build backend images / build identity-svc (push) Failing after 58s
Build backend images / build notification-svc (push) Failing after 55s
Build backend images / build render-svc (push) Failing after 59s
Build backend images / build studio-svc (push) Failing after 48s
feat(render+node-agent+admin): install fonts on all render nodes + verify
Push a font once → every node installs it → admin sees per-node status.

- render-svc: font_requests + node_fonts tables (mig 25); admin GET/POST/DELETE
  /v1/node-fonts (with per-node status matrix); internal (HMAC) GET pending +
  POST status for node-agents
- node-agent: fontSyncLoop polls pending fonts every 60s, downloads, installs
  (Windows Fonts dir + registry / macOS / linux fc-cache), reports Installed/Failed
- gateway: /v1/node-fonts/* → render
- admin /admin/node-fonts: upload a .ttf/.otf → install on all nodes; per-node
  Installed/Pending/Failed badges + counts + delete

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 06:33:48 +03:30

108 lines
3.4 KiB
Go

package handlers
import (
"net/http"
"github.com/flatrender/render-svc/internal/db"
"github.com/flatrender/render-svc/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type FontHandler struct {
store *db.Store
}
func NewFontHandler(store *db.Store) *FontHandler {
return &FontHandler{store: store}
}
// ── Admin ────────────────────────────────────────────────────────────────────
// GET /v1/node-fonts
func (h *FontHandler) List(c *gin.Context) {
items, err := h.store.ListFontRequestsWithStatus(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()})
return
}
if items == nil {
items = []models.FontRequestWithStatus{}
}
c.JSON(http.StatusOK, gin.H{"items": items})
}
// POST /v1/node-fonts
func (h *FontHandler) Create(c *gin.Context) {
var req models.CreateFontRequestBody
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()})
return
}
fr, err := h.store.CreateFontRequest(c.Request.Context(), req.Name, req.SystemName, req.FileURL)
if err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()})
return
}
c.JSON(http.StatusCreated, fr)
}
// DELETE /v1/node-fonts/:id
func (h *FontHandler) Delete(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid id"})
return
}
if err := h.store.DeleteFontRequest(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()})
return
}
c.Status(http.StatusNoContent)
}
// ── Internal (node agents, HMAC auth) ────────────────────────────────────────
// GET /v1/internal/nodes/:node_id/fonts/pending
func (h *FontHandler) Pending(c *gin.Context) {
nodeID, err := uuid.Parse(c.Param("node_id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid node_id"})
return
}
fonts, err := h.store.PendingFontsForNode(c.Request.Context(), nodeID)
if err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()})
return
}
if fonts == nil {
fonts = []models.PendingFont{}
}
c.JSON(http.StatusOK, gin.H{"fonts": fonts})
}
// POST /v1/internal/nodes/:node_id/fonts/:request_id/status
func (h *FontHandler) Report(c *gin.Context) {
nodeID, err := uuid.Parse(c.Param("node_id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid node_id"})
return
}
reqID, err := uuid.Parse(c.Param("request_id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid request_id"})
return
}
var body models.FontStatusReport
_ = c.ShouldBindJSON(&body)
status := body.Status
if status != "Installed" {
status = "Failed"
}
if err := h.store.ReportFontStatus(c.Request.Context(), nodeID, reqID, status, body.Error); err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()})
return
}
c.Status(http.StatusNoContent)
}