feat(render+node-agent+admin): install fonts on all render nodes + verify
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

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>
This commit is contained in:
soroush.asadi
2026-06-03 06:33:48 +03:30
parent ca0c05db10
commit 7f2f65dd8a
14 changed files with 648 additions and 3 deletions
@@ -61,6 +61,48 @@ func decodeJSON(resp *http.Response, out any) error {
return json.NewDecoder(resp.Body).Decode(out)
}
// ── Fonts ───────────────────────────────────────────────────────────────────
type PendingFont struct {
ID string `json:"id"`
Name string `json:"name"`
SystemName string `json:"system_name"`
FileURL string `json:"file_url"`
}
type pendingFontsResp struct {
Fonts []PendingFont `json:"fonts"`
}
// PendingFonts returns fonts this node still needs to install.
func (c *Client) PendingFonts(ctx context.Context, nodeID string) ([]PendingFont, error) {
resp, err := c.do(ctx, http.MethodGet, fmt.Sprintf("/v1/internal/nodes/%s/fonts/pending", nodeID), nil)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("pending fonts status %d", resp.StatusCode)
}
var out pendingFontsResp
if err := decodeJSON(resp, &out); err != nil {
return nil, err
}
return out.Fonts, nil
}
// ReportFont reports a font install result (Installed | Failed).
func (c *Client) ReportFont(ctx context.Context, nodeID, requestID, status, errMsg string) error {
resp, err := c.do(ctx, http.MethodPost,
fmt.Sprintf("/v1/internal/nodes/%s/fonts/%s/status", nodeID, requestID),
map[string]string{"status": status, "error": errMsg})
if err != nil {
return err
}
resp.Body.Close()
return nil
}
// ── Domain types ──────────────────────────────────────────────────────────────
// OnlineRequest is sent once on startup to mark the node Ready.