Files
flatrender/services/render/internal/db/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

145 lines
4.1 KiB
Go

package db
import (
"context"
"github.com/flatrender/render-svc/internal/models"
"github.com/google/uuid"
)
func (s *Store) CreateFontRequest(ctx context.Context, name, systemName, fileURL string) (*models.FontRequest, error) {
var fr models.FontRequest
var sys *string
if systemName != "" {
sys = &systemName
}
err := s.pool.QueryRow(ctx,
`INSERT INTO render.font_requests (name, system_name, file_url)
VALUES ($1, $2, $3) RETURNING id, name, system_name, file_url, created_at`,
name, sys, fileURL).Scan(&fr.ID, &fr.Name, &fr.SystemName, &fr.FileURL, &fr.CreatedAt)
return &fr, err
}
func (s *Store) DeleteFontRequest(ctx context.Context, id uuid.UUID) error {
_, err := s.pool.Exec(ctx, `DELETE FROM render.font_requests WHERE id = $1`, id)
return err
}
// ListFontRequestsWithStatus returns every font request with a per-node status matrix
// (nodes without an explicit row are reported as Pending).
func (s *Store) ListFontRequestsWithStatus(ctx context.Context) ([]models.FontRequestWithStatus, error) {
// 1) active nodes
type node struct {
id uuid.UUID
name string
}
var nodes []node
rows, err := s.pool.Query(ctx, `SELECT id, name FROM render.render_nodes ORDER BY name`)
if err != nil {
return nil, err
}
for rows.Next() {
var n node
if err := rows.Scan(&n.id, &n.name); err != nil {
rows.Close()
return nil, err
}
nodes = append(nodes, n)
}
rows.Close()
// 2) requests
var reqs []models.FontRequest
rrows, err := s.pool.Query(ctx, `SELECT id, name, system_name, file_url, created_at FROM render.font_requests ORDER BY created_at DESC`)
if err != nil {
return nil, err
}
for rrows.Next() {
var fr models.FontRequest
if err := rrows.Scan(&fr.ID, &fr.Name, &fr.SystemName, &fr.FileURL, &fr.CreatedAt); err != nil {
rrows.Close()
return nil, err
}
reqs = append(reqs, fr)
}
rrows.Close()
// 3) explicit per-node statuses
type key struct {
req uuid.UUID
node uuid.UUID
}
statuses := map[key]models.NodeFontStatus{}
srows, err := s.pool.Query(ctx, `SELECT font_request_id, node_id, status, error, updated_at FROM render.node_fonts`)
if err != nil {
return nil, err
}
for srows.Next() {
var reqID, nodeID uuid.UUID
var st models.NodeFontStatus
if err := srows.Scan(&reqID, &nodeID, &st.Status, &st.Error, &st.UpdatedAt); err != nil {
srows.Close()
return nil, err
}
st.NodeID = nodeID
statuses[key{reqID, nodeID}] = st
}
srows.Close()
out := make([]models.FontRequestWithStatus, 0, len(reqs))
for _, fr := range reqs {
item := models.FontRequestWithStatus{FontRequest: fr, TotalNodes: len(nodes)}
for _, n := range nodes {
st, ok := statuses[key{fr.ID, n.id}]
if !ok {
st = models.NodeFontStatus{NodeID: n.id, Status: "Pending"}
}
st.NodeName = n.name
if st.Status == "Installed" {
item.InstalledCount++
}
item.Nodes = append(item.Nodes, st)
}
out = append(out, item)
}
return out, nil
}
// PendingFontsForNode returns fonts this node has not yet Installed (Pending or Failed → retry).
func (s *Store) PendingFontsForNode(ctx context.Context, nodeID uuid.UUID) ([]models.PendingFont, error) {
rows, err := s.pool.Query(ctx, `
SELECT fr.id, fr.name, fr.system_name, fr.file_url
FROM render.font_requests fr
WHERE NOT EXISTS (
SELECT 1 FROM render.node_fonts nf
WHERE nf.font_request_id = fr.id AND nf.node_id = $1 AND nf.status = 'Installed'
)`, nodeID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []models.PendingFont
for rows.Next() {
var f models.PendingFont
if err := rows.Scan(&f.ID, &f.Name, &f.SystemName, &f.FileURL); err != nil {
return nil, err
}
out = append(out, f)
}
return out, rows.Err()
}
func (s *Store) ReportFontStatus(ctx context.Context, nodeID, requestID uuid.UUID, status, errMsg string) error {
var e *string
if errMsg != "" {
e = &errMsg
}
_, err := s.pool.Exec(ctx, `
INSERT INTO render.node_fonts (node_id, font_request_id, status, error, updated_at)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT (node_id, font_request_id)
DO UPDATE SET status = EXCLUDED.status, error = EXCLUDED.error, updated_at = NOW()`,
nodeID, requestID, status, e)
return err
}