feat(render): node-agent AE snapshot runner (Epic C2) + colour render-binding (Epic B)
Build backend images / build content-svc (push) Failing after 13s
Build backend images / build file-svc (push) Failing after 53s
Build backend images / build gateway (push) Failing after 1m22s
Build backend images / build identity-svc (push) Failing after 19s
Build backend images / build notification-svc (push) Failing after 21s
Build backend images / build render-svc (push) Failing after 20s
Build backend images / build studio-svc (push) Failing after 1m6s

C2 — real-AE scene snapshots on the node:
- node-agent: runner/snapshot.go RunSnapshot (aerender -comp <key> -s f -e f
  → findRenderedOutput → ffmpeg -frames:v 1 PNG); client ClaimSnapshot /
  GetSnapshotUploadURL / ReportSnapshotResult / ReportSnapshotFail; snapshotLoop +
  pollSnapshotOnce mirroring the scan loop (reuses the AE-exclusive lock).
- render-svc: GetSnapshotJobMeta + UploadURL handler presigns a PUT to the
  public-read user-uploads bucket at snapshots/{project}/{scene}.png and returns a
  permanent public_url (not the time-limited export presign); MINIO_UPLOAD_BUCKET +
  MINIO_PUBLIC_URL config + compose env + /snapshot/:id/upload-url route.

Epic B — bind edited colours into the render:
- render-svc GetRenderBindings UNIONs studio.saved_shared_colors +
  saved_scene_colors (type 'color') so the node writes them before render.
- node-agent binder.go routes type:"color" bindings into the bind-spec colors[]
  array that bind.jsx already applies to the frshare colour layers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-11 18:08:43 +03:30
parent 8488acb115
commit 6cf8716d7e
9 changed files with 329 additions and 5 deletions
@@ -1,7 +1,9 @@
package handlers
import (
"fmt"
"net/http"
"time"
"github.com/flatrender/render-svc/internal/db"
"github.com/flatrender/render-svc/internal/models"
@@ -17,10 +19,12 @@ type SnapshotJobHandler struct {
store *db.Store
minio *minio.Client
templatesBucket string
uploadBucket string // public-read bucket snapshots land in (e.g. user-uploads)
publicBase string // browser-reachable base, e.g. http://172.28.144.1:9000
}
func NewSnapshotJobHandler(store *db.Store, mc *minio.Client, templatesBucket string) *SnapshotJobHandler {
return &SnapshotJobHandler{store: store, minio: mc, templatesBucket: templatesBucket}
func NewSnapshotJobHandler(store *db.Store, mc *minio.Client, templatesBucket, uploadBucket, publicBase string) *SnapshotJobHandler {
return &SnapshotJobHandler{store: store, minio: mc, templatesBucket: templatesBucket, uploadBucket: uploadBucket, publicBase: publicBase}
}
// POST /v1/scene-snapshots/:project_id (admin) → queue one job per active scene.
@@ -90,6 +94,33 @@ func (h *SnapshotJobHandler) Claim(c *gin.Context) {
})
}
// POST /v1/internal/snapshot/:id/upload-url (node, HMAC)
// Presigns a PUT to the public-read uploads bucket and returns the permanent
// public URL the node should report back once the still is uploaded.
func (h *SnapshotJobHandler) UploadURL(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
}
pid, sceneKey, merr := h.store.GetSnapshotJobMeta(c.Request.Context(), id)
if merr != nil {
c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: "snapshot job not found"})
return
}
objectKey := fmt.Sprintf("snapshots/%s/%s.png", pid, sceneKey)
put, perr := h.minio.PresignedPutObject(c.Request.Context(), h.uploadBucket, objectKey, 15*time.Minute)
if perr != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "presign_failed", Message: perr.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"upload_url": put.String(),
"object_key": objectKey,
"public_url": fmt.Sprintf("%s/%s/%s", h.publicBase, h.uploadBucket, objectKey),
})
}
// POST /v1/internal/snapshot/:id/result (node, HMAC) body {image_url}
func (h *SnapshotJobHandler) Result(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))