feat: live render preview — node agent pushes PNG frames, frontend displays them in real time
render-svc: - db.UpdateJobPreview(): writes base64 PNG to render_jobs.image_preview_b64 (only on active jobs; Done/Failed/Cancelled rows ignored) - POST /v1/internal/render/jobs/:job_id/preview — node agent endpoint - Route registered under /v1/internal (nodeAuth) node-agent: - runner.PreviewFn callback type alongside ProgressFn - runner.preview.go: GeneratePreviewB64(percent, quality, resolution) — pure stdlib (image/png + encoding/base64), no external deps — 320×180 dark frame with animated progress bar + colored indicator dots - mock render: pushes a preview frame at every step (5→95%) - real AE render: pushes a preview frame every 30s - client.UpdatePreview(): POST /v1/internal/render/jobs/:job_id/preview - main.go: onPreview callback wires client.UpdatePreview() into runner.Run() frontend: - render-jobs.ts: RenderJobRow.preview_b64 field; read from progress endpoint - status/route.ts: previewB64 included in JSON response - RenderModal: aspect-ratio preview pane during polling — shows spinner until first frame arrives, then live-updates with each poll (every 3s); step label overlaid as badge bottom-right Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -519,6 +519,18 @@ func (s *Store) ClaimJob(ctx context.Context, nodeID uuid.UUID, region string) (
|
||||
return s.getJobByIDInternal(ctx, jobID)
|
||||
}
|
||||
|
||||
// UpdateJobPreview stores a base64-encoded preview frame for a running job.
|
||||
// Called by the node agent every N frames to power the live preview UI.
|
||||
func (s *Store) UpdateJobPreview(ctx context.Context, jobID uuid.UUID, imageB64 string) error {
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE render.render_jobs
|
||||
SET image_preview_b64 = $1, updated_at = NOW()
|
||||
WHERE id = $2
|
||||
AND step NOT IN ('Done'::render_step, 'Failed'::render_step, 'Cancelled'::render_step)`,
|
||||
imageB64, jobID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) CancelJob(ctx context.Context, id, userID uuid.UUID) (bool, error) {
|
||||
tag, err := s.pool.Exec(ctx, `
|
||||
UPDATE render.render_jobs
|
||||
|
||||
@@ -198,6 +198,29 @@ func (h *InternalHandler) ReplicaReady(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// POST /v1/internal/render/jobs/:job_id/preview
|
||||
// Node agent pushes a base64-encoded frame image so the frontend can show
|
||||
// a live preview while the job is rendering.
|
||||
func (h *InternalHandler) Preview(c *gin.Context) {
|
||||
jobID, err := uuid.Parse(c.Param("job_id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid job_id"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
ImageB64 string `json:"image_b64" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.store.UpdateJobPreview(c.Request.Context(), jobID, req.ImageB64); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// POST /v1/internal/render/jobs/claim
|
||||
// Node agent calls this to atomically claim the next queued job.
|
||||
// Returns 204 when there is nothing queued (agent should back off and retry).
|
||||
|
||||
Reference in New Issue
Block a user