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
+40 -1
View File
@@ -121,9 +121,10 @@ func main() {
// Main loops
var wg sync.WaitGroup
wg.Add(2)
wg.Add(3)
go func() { defer wg.Done(); agent.heartbeatLoop(ctx) }()
go func() { defer wg.Done(); agent.pollLoop(ctx) }()
go func() { defer wg.Done(); agent.fontSyncLoop(ctx) }()
wg.Wait()
log.Printf("shutdown complete")
}
@@ -144,6 +145,44 @@ func (a *Agent) registerOnline(ctx context.Context) error {
return nil
}
// ── Font sync loop ────────────────────────────────────────────────────────────
// Periodically installs any fonts the orchestrator wants on this node and reports
// per-font status, so the admin can verify installation.
func (a *Agent) fontSyncLoop(ctx context.Context) {
ticker := time.NewTicker(60 * time.Second)
defer ticker.Stop()
a.syncFonts(ctx) // run once on startup
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
a.syncFonts(ctx)
}
}
}
func (a *Agent) syncFonts(ctx context.Context) {
fonts, err := a.orch.PendingFonts(ctx, a.cfg.NodeID)
if err != nil {
return // transient; try again next tick
}
for _, f := range fonts {
name := f.SystemName
if name == "" {
name = f.Name
}
if err := runner.InstallFont(ctx, f.FileURL, name); err != nil {
log.Printf("font install failed (%s): %v", f.Name, err)
_ = a.orch.ReportFont(ctx, a.cfg.NodeID, f.ID, "Failed", err.Error())
continue
}
log.Printf("font installed: %s", f.Name)
_ = a.orch.ReportFont(ctx, a.cfg.NodeID, f.ID, "Installed", "")
}
}
// ── Heartbeat loop ────────────────────────────────────────────────────────────
func (a *Agent) heartbeatLoop(ctx context.Context) {