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
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:
@@ -36,6 +36,13 @@ func main() {
|
||||
minioUseSSL := getEnv("MINIO_USE_SSL", "false") == "true"
|
||||
minioBucket := getEnv("MINIO_BUCKET", "flatrender-exports")
|
||||
minioTemplatesBucket := getEnv("MINIO_TEMPLATES_BUCKET", "flatrender-templates")
|
||||
minioUploadBucket := getEnv("MINIO_UPLOAD_BUCKET", "user-uploads") // public-read — scene snapshots
|
||||
minioPublicBase := getEnv("MINIO_PUBLIC_URL", func() string {
|
||||
if minioUseSSL {
|
||||
return "https://" + minioEndpoint
|
||||
}
|
||||
return "http://" + minioEndpoint
|
||||
}())
|
||||
notificationURL := getEnv("NOTIFICATION_URL", "http://localhost:8080")
|
||||
identityURL := getEnv("IDENTITY_URL", "")
|
||||
serviceToken := getEnv("SERVICE_TOKEN", "internal-service-secret")
|
||||
@@ -82,7 +89,7 @@ func main() {
|
||||
fontH := handlers.NewFontHandler(store)
|
||||
bundleH := handlers.NewTemplateBundleHandler(mc, minioTemplatesBucket)
|
||||
scanH := handlers.NewScanHandler(store, mc, minioTemplatesBucket)
|
||||
snapJobH := handlers.NewSnapshotJobHandler(store, mc, minioTemplatesBucket)
|
||||
snapJobH := handlers.NewSnapshotJobHandler(store, mc, minioTemplatesBucket, minioUploadBucket, minioPublicBase)
|
||||
internalH := handlers.NewInternalHandler(store, notifyClient, mc, minioTemplatesBucket, minioBucket)
|
||||
|
||||
// ── Dev mock worker (no AE node needed) ────────────────────────────────────
|
||||
@@ -217,6 +224,7 @@ func main() {
|
||||
|
||||
// AE scene snapshots (node claims, renders one frame, posts the image URL)
|
||||
internal.POST("/snapshot/claim", snapJobH.Claim)
|
||||
internal.POST("/snapshot/:id/upload-url", snapJobH.UploadURL)
|
||||
internal.POST("/snapshot/:id/result", snapJobH.Result)
|
||||
internal.POST("/snapshot/:id/fail", snapJobH.Fail)
|
||||
|
||||
|
||||
@@ -674,7 +674,19 @@ func (s *Store) GetRenderBindings(ctx context.Context, savedProjectID uuid.UUID)
|
||||
SELECT c.key, c.type, COALESCE(c.value, '')
|
||||
FROM studio.saved_scene_contents c
|
||||
JOIN studio.saved_scenes s ON s.id = c.saved_scene_id
|
||||
WHERE s.saved_project_id = $1 AND c.value IS NOT NULL AND c.value <> ''`,
|
||||
WHERE s.saved_project_id = $1 AND c.value IS NOT NULL AND c.value <> ''
|
||||
UNION
|
||||
-- project-wide shared colours (frshare/frd_* layers): bind.jsx writes these
|
||||
-- into the frshare comp's text layers so template expressions propagate them.
|
||||
SELECT sc.element_key, 'color', sc.value
|
||||
FROM studio.saved_shared_colors sc
|
||||
WHERE sc.saved_project_id = $1 AND sc.value IS NOT NULL AND sc.value <> ''
|
||||
UNION
|
||||
-- per-scene colours (frl_c* layers)
|
||||
SELECT cc.element_key, 'color', cc.value
|
||||
FROM studio.saved_scene_colors cc
|
||||
JOIN studio.saved_scenes s2 ON s2.id = cc.saved_scene_id
|
||||
WHERE s2.saved_project_id = $1 AND cc.value IS NOT NULL AND cc.value <> ''`,
|
||||
savedProjectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -123,3 +123,13 @@ func (s *Store) SetSnapshotError(ctx context.Context, id uuid.UUID, msg string)
|
||||
id, msg)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetSnapshotJobMeta returns the project id + scene key for a job (to build the
|
||||
// object key the node uploads its rendered still to).
|
||||
func (s *Store) GetSnapshotJobMeta(ctx context.Context, id uuid.UUID) (uuid.UUID, string, error) {
|
||||
var pid uuid.UUID
|
||||
var key string
|
||||
err := s.pool.QueryRow(ctx,
|
||||
`SELECT project_id, scene_key FROM render.snapshot_jobs WHERE id = $1`, id).Scan(&pid, &key)
|
||||
return pid, key, err
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user