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 }