feat: V2 microservices stack — backend services, gateway, JWT auth
Add full V2 architecture: identity, content, studio (.NET 10) and file, render, notification, gateway (Go) services with vendored deps, plus DB migrations, event/API contracts, and an init-db script. Wire the Next.js frontend to the gateway: server-side JWT auth routes (login/register/refresh/logout/me), gateway fetch helper, and session/ cookie/jwt helpers under src/lib. Containerize the stack via docker-compose.v2.yml and per-service Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via next/font/local to avoid Google Fonts (geo-blocked). Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,431 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ── Enums ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
// NodeStatus
|
||||
NodeStatusReady = "Ready"
|
||||
NodeStatusBusy = "Busy"
|
||||
NodeStatusOffline = "Offline"
|
||||
NodeStatusMaintenance = "Maintenance"
|
||||
NodeStatusCrashed = "Crashed"
|
||||
NodeStatusUpdating = "Updating"
|
||||
NodeStatusDisabled = "Disabled"
|
||||
|
||||
// NodeKind
|
||||
NodeKindShared = "Shared"
|
||||
NodeKindDedicated = "Dedicated"
|
||||
NodeKindSpot = "Spot"
|
||||
|
||||
// RenderStep
|
||||
StepQueued = "Queued"
|
||||
StepPreparing = "Preparing"
|
||||
StepTemplateCache = "TemplateCache"
|
||||
StepJsxGen = "JsxGen"
|
||||
StepMusic = "Music"
|
||||
StepRendering = "Rendering"
|
||||
StepValidating = "Validating"
|
||||
StepRepairing = "Repairing"
|
||||
StepOptimisation = "Optimisation"
|
||||
StepVideo = "Video"
|
||||
StepMixing = "Mixing"
|
||||
StepFinal = "Final"
|
||||
StepUploading = "Uploading"
|
||||
StepDone = "Done"
|
||||
StepFailed = "Failed"
|
||||
StepCancelled = "Cancelled"
|
||||
|
||||
// PriceKind
|
||||
PriceKindFree = "Free"
|
||||
PriceKindPreview = "Preview"
|
||||
PriceKindCash = "Cash"
|
||||
PriceKindPlan = "Plan"
|
||||
PriceKindSnapshot = "Snapshot"
|
||||
PriceKindReseller = "Reseller"
|
||||
|
||||
// RenderQuality
|
||||
QualityLow = "Low"
|
||||
QualityMedium = "Medium"
|
||||
QualityHigh = "High"
|
||||
QualityFull = "Full"
|
||||
QualityLossless = "Lossless"
|
||||
|
||||
// FrameJobStatus
|
||||
FrameJobPending = "Pending"
|
||||
FrameJobRendering = "Rendering"
|
||||
FrameJobValidated = "Validated"
|
||||
FrameJobRepairing = "Repairing"
|
||||
FrameJobConverting = "Converting"
|
||||
FrameJobDone = "Done"
|
||||
FrameJobFailed = "Failed"
|
||||
|
||||
// PriorityQueue
|
||||
QueueSnapshot = "snapshot"
|
||||
QueueVIP = "vip"
|
||||
QueuePaid = "paid"
|
||||
QueuePreview = "preview"
|
||||
QueueMockup = "mockup"
|
||||
QueueVoiceover = "voiceover"
|
||||
|
||||
// Export types
|
||||
ExportCreateRender = "Render"
|
||||
ExportCreateUpload = "Upload"
|
||||
ExportCreateSnapshot = "Snapshot"
|
||||
ExportCreateReupload = "Reupload"
|
||||
|
||||
ExportFileVideo = "Video"
|
||||
ExportFileImage = "Image"
|
||||
ExportFileAudio = "Audio"
|
||||
ExportFileGIF = "GIF"
|
||||
ExportFilePDF = "PDF"
|
||||
)
|
||||
|
||||
// ── Domain entities ──────────────────────────────────────────────────────────
|
||||
|
||||
type RenderNode struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Region string `json:"region"`
|
||||
NodeIP string `json:"node_ip"`
|
||||
WorkerPort int `json:"worker_port"`
|
||||
PublicEndpoint *string `json:"public_endpoint,omitempty"`
|
||||
RamGB *int `json:"ram_gb,omitempty"`
|
||||
CPUCores *int `json:"cpu_cores,omitempty"`
|
||||
GPUModel *string `json:"gpu_model,omitempty"`
|
||||
StorageGB *int `json:"storage_gb,omitempty"`
|
||||
CurrentAEVersion string `json:"current_ae_version"`
|
||||
AvailableAEVersions []string `json:"available_ae_versions"`
|
||||
NodeAgentVersion *string `json:"node_agent_version,omitempty"`
|
||||
NodeKind string `json:"node_kind"`
|
||||
OwnerUserID *uuid.UUID `json:"owner_user_id,omitempty"`
|
||||
OwnerTenantID *uuid.UUID `json:"owner_tenant_id,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CurrentJobID *uuid.UUID `json:"current_job_id,omitempty"`
|
||||
CurrentFrameJobID *uuid.UUID `json:"current_frame_job_id,omitempty"`
|
||||
JobStartedAt *time.Time `json:"job_started_at,omitempty"`
|
||||
LastHeartbeatAt *time.Time `json:"last_heartbeat_at,omitempty"`
|
||||
LastCPUPct *int `json:"last_cpu_pct,omitempty"`
|
||||
LastRAMAvailableMB *int `json:"last_ram_available_mb,omitempty"`
|
||||
AERunning bool `json:"ae_running"`
|
||||
LifetimeTaskCount int64 `json:"lifetime_task_count"`
|
||||
LifetimeCrashCount int `json:"lifetime_crash_count"`
|
||||
ConsecutiveFailures int `json:"consecutive_failures"`
|
||||
Priority int `json:"priority"`
|
||||
IsActive bool `json:"is_active"`
|
||||
AcceptsNewJobs bool `json:"accepts_new_jobs"`
|
||||
LastMaintenanceAt *time.Time `json:"last_maintenance_at,omitempty"`
|
||||
NextMaintenanceAt *time.Time `json:"next_maintenance_at,omitempty"`
|
||||
MaintenanceReason *string `json:"maintenance_reason,omitempty"`
|
||||
CachedTemplateMD5s []string `json:"cached_template_md5s"`
|
||||
CacheUsedGB int `json:"cache_used_gb"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type RenderJob struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
SavedProjectID uuid.UUID `json:"saved_project_id"`
|
||||
OriginalProjectID uuid.UUID `json:"original_project_id"`
|
||||
ProjectName *string `json:"project_name,omitempty"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
ExternalJobID *string `json:"external_job_id,omitempty"`
|
||||
PriorityQueue string `json:"priority_queue"`
|
||||
PriorityScore int `json:"priority_score"`
|
||||
Step string `json:"step"`
|
||||
RenderProgress int `json:"render_progress"`
|
||||
ConvertProgress int `json:"convert_progress"`
|
||||
ImagePreviewB64 *string `json:"image_preview_b64,omitempty"`
|
||||
PriceType string `json:"price_type"`
|
||||
PaidPriceMinor int64 `json:"paid_price_minor"`
|
||||
DiscountCode *string `json:"discount_code,omitempty"`
|
||||
SupportFlatrender bool `json:"support_flatrender"`
|
||||
Mode string `json:"mode"`
|
||||
Quality string `json:"quality"`
|
||||
Resolution string `json:"resolution"`
|
||||
RHeight int `json:"r_height"`
|
||||
FrameRate int `json:"frame_rate"`
|
||||
Is60FPS bool `json:"is_60_fps"`
|
||||
DurationSec float64 `json:"duration_sec"`
|
||||
ExportDurationSec *float64 `json:"export_duration_sec,omitempty"`
|
||||
HasMusic bool `json:"has_music"`
|
||||
HasSFX bool `json:"has_sfx"`
|
||||
HasVoiceover bool `json:"has_voiceover"`
|
||||
MusicVolume *float64 `json:"music_volume,omitempty"`
|
||||
SFXVolume *float64 `json:"sfx_volume,omitempty"`
|
||||
VoiceoverVolume *float64 `json:"voiceover_volume,omitempty"`
|
||||
RenderNodeCount int `json:"render_node_count"`
|
||||
CurrentActiveNodes int `json:"current_active_nodes"`
|
||||
Region *string `json:"region,omitempty"`
|
||||
TellMeWhenDone bool `json:"tell_me_when_done"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
MaxRetries int `json:"max_retries"`
|
||||
RepairAttempts int `json:"repair_attempts"`
|
||||
FailedMessage *string `json:"failed_message,omitempty"`
|
||||
FailedAtStep *string `json:"failed_at_step,omitempty"`
|
||||
ExportID *uuid.UUID `json:"export_id,omitempty"`
|
||||
TaskStartDate time.Time `json:"task_start_date"`
|
||||
QueuedAt time.Time `json:"queued_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type FrameJob struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
RenderJobID uuid.UUID `json:"render_job_id"`
|
||||
NodeID uuid.UUID `json:"node_id"`
|
||||
StartFrame int `json:"start_frame"`
|
||||
EndFrame int `json:"end_frame"`
|
||||
CollectFrameCount int `json:"collect_frame_count"`
|
||||
OrderValue int `json:"order_value"`
|
||||
FolderName string `json:"folder_name"`
|
||||
ConvertURL *string `json:"convert_url,omitempty"`
|
||||
Status string `json:"status"`
|
||||
FramesRendered int `json:"frames_rendered"`
|
||||
FramesValidated int `json:"frames_validated"`
|
||||
Attempt int `json:"attempt"`
|
||||
LastError *string `json:"last_error,omitempty"`
|
||||
OutputMP4URL *string `json:"output_mp4_url,omitempty"`
|
||||
AssignedAt time.Time `json:"assigned_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
LastProgressAt *time.Time `json:"last_progress_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Snapshot struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
SavedProjectID uuid.UUID `json:"saved_project_id"`
|
||||
SceneKey string `json:"scene_key"`
|
||||
FrameNumber int `json:"frame_number"`
|
||||
InputsHash string `json:"inputs_hash"`
|
||||
Status string `json:"status"`
|
||||
RenderNodeID *uuid.UUID `json:"render_node_id,omitempty"`
|
||||
ImageURL *string `json:"image_url,omitempty"`
|
||||
ThumbnailURL *string `json:"thumbnail_url,omitempty"`
|
||||
Width *int `json:"width,omitempty"`
|
||||
Height *int `json:"height,omitempty"`
|
||||
SizeBytes *int64 `json:"size_bytes,omitempty"`
|
||||
RequestedAt time.Time `json:"requested_at"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
DurationMS *int `json:"duration_ms,omitempty"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
ErrorMessage *string `json:"error_message,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type Export struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
SavedProjectID uuid.UUID `json:"saved_project_id"`
|
||||
ProjectID uuid.UUID `json:"project_id"`
|
||||
RenderJobID *uuid.UUID `json:"render_job_id,omitempty"`
|
||||
Image *string `json:"image,omitempty"`
|
||||
Path string `json:"path"`
|
||||
FileExtension string `json:"file_extension"`
|
||||
FileType string `json:"file_type"`
|
||||
RenderQuality string `json:"render_quality"`
|
||||
CreateType string `json:"create_type"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
DurationSec *float64 `json:"duration_sec,omitempty"`
|
||||
Width *int `json:"width,omitempty"`
|
||||
Height *int `json:"height,omitempty"`
|
||||
ProduceDate time.Time `json:"produce_date"`
|
||||
AutoDeleteDate time.Time `json:"auto_delete_date"`
|
||||
DeleteNotified bool `json:"delete_notified"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
}
|
||||
|
||||
type ExportFile struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ExportID uuid.UUID `json:"export_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||
Path string `json:"path"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
FileType string `json:"file_type"`
|
||||
Width *int `json:"width,omitempty"`
|
||||
Height *int `json:"height,omitempty"`
|
||||
Sort int `json:"sort"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type NodeHealthLog struct {
|
||||
ID int64 `json:"id"`
|
||||
NodeID uuid.UUID `json:"node_id"`
|
||||
RecordedAt time.Time `json:"recorded_at"`
|
||||
Status string `json:"status"`
|
||||
CPUPct *int `json:"cpu_pct,omitempty"`
|
||||
RAMAvailableMB *int `json:"ram_available_mb,omitempty"`
|
||||
AERunning *bool `json:"ae_running,omitempty"`
|
||||
CurrentJobID *uuid.UUID `json:"current_job_id,omitempty"`
|
||||
CurrentFrame *int `json:"current_frame,omitempty"`
|
||||
CacheUsedGB *int `json:"cache_used_gb,omitempty"`
|
||||
}
|
||||
|
||||
type NodeCrash struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
NodeID uuid.UUID `json:"node_id"`
|
||||
RenderJobID *uuid.UUID `json:"render_job_id,omitempty"`
|
||||
FrameJobID *uuid.UUID `json:"frame_job_id,omitempty"`
|
||||
CrashedAt time.Time `json:"crashed_at"`
|
||||
LastKnownFrame *int `json:"last_known_frame,omitempty"`
|
||||
CrashSignal *string `json:"crash_signal,omitempty"`
|
||||
ErrorLog *string `json:"error_log,omitempty"`
|
||||
LogFileURL *string `json:"log_file_url,omitempty"`
|
||||
AutoRecovered bool `json:"auto_recovered"`
|
||||
RecoveryAction *string `json:"recovery_action,omitempty"`
|
||||
RecoveredAt *time.Time `json:"recovered_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type NodeUpdate struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
UpdateFileName string `json:"update_file_name"`
|
||||
UpdateNumber int `json:"update_number"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
TargetAEVersion *string `json:"target_ae_version,omitempty"`
|
||||
InUpdateQueue bool `json:"in_update_queue"`
|
||||
RolledOutToNodeIDs []uuid.UUID `json:"rolled_out_to_node_ids"`
|
||||
LastUpdateQueueDate *time.Time `json:"last_update_queue_date,omitempty"`
|
||||
CreateDate time.Time `json:"create_date"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ── Request / Response types ─────────────────────────────────────────────────
|
||||
|
||||
type PagedResponse[T any] struct {
|
||||
Data []T `json:"data"`
|
||||
Meta PaginationMeta `json:"meta"`
|
||||
}
|
||||
|
||||
type PaginationMeta struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Total int64 `json:"total"`
|
||||
HasMore bool `json:"has_more"`
|
||||
}
|
||||
|
||||
type RenderJobCreateRequest struct {
|
||||
SavedProjectID uuid.UUID `json:"saved_project_id" binding:"required"`
|
||||
Quality string `json:"quality" binding:"required"`
|
||||
Resolution string `json:"resolution" binding:"required"`
|
||||
FrameRate *int `json:"frame_rate"`
|
||||
Is60FPS *bool `json:"is_60_fps"`
|
||||
PriceType *string `json:"price_type"`
|
||||
DiscountCode *string `json:"discount_code"`
|
||||
SupportFlatrender *bool `json:"support_flatrender"`
|
||||
TellMeWhenDone *bool `json:"tell_me_when_done"`
|
||||
PreferredRegion *string `json:"preferred_region"`
|
||||
}
|
||||
|
||||
type SnapshotCreateRequest struct {
|
||||
SavedProjectID uuid.UUID `json:"saved_project_id" binding:"required"`
|
||||
SceneKey string `json:"scene_key" binding:"required"`
|
||||
FrameNumber int `json:"frame_number" binding:"min=0"`
|
||||
}
|
||||
|
||||
type NodeCreateRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Region string `json:"region" binding:"required"`
|
||||
NodeIP string `json:"node_ip" binding:"required"`
|
||||
WorkerPort int `json:"worker_port" binding:"required"`
|
||||
CurrentAEVersion string `json:"current_ae_version" binding:"required"`
|
||||
RamGB *int `json:"ram_gb"`
|
||||
CPUCores *int `json:"cpu_cores"`
|
||||
NodeKind *string `json:"node_kind"`
|
||||
OwnerUserID *uuid.UUID `json:"owner_user_id"`
|
||||
Priority *int `json:"priority"`
|
||||
}
|
||||
|
||||
type NodePatchRequest struct {
|
||||
Priority *int `json:"priority"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
AcceptsNewJobs *bool `json:"accepts_new_jobs"`
|
||||
NodeKind *string `json:"node_kind"`
|
||||
OwnerUserID *uuid.UUID `json:"owner_user_id"`
|
||||
NextMaintenanceAt *time.Time `json:"next_maintenance_at"`
|
||||
MaintenanceReason *string `json:"maintenance_reason"`
|
||||
}
|
||||
|
||||
type NodeHeartbeatRequest struct {
|
||||
NodeID uuid.UUID `json:"node_id"`
|
||||
Status string `json:"status"`
|
||||
CPUPct *int `json:"cpu_pct"`
|
||||
RAMAvailableMB *int `json:"ram_available_mb"`
|
||||
AERunning *bool `json:"ae_running"`
|
||||
CurrentJobID *uuid.UUID `json:"current_job_id"`
|
||||
CurrentFrame *int `json:"current_frame"`
|
||||
CacheUsedGB *int `json:"cache_used_gb"`
|
||||
}
|
||||
|
||||
type NodeOnlineRequest struct {
|
||||
NodeAgentVersion string `json:"node_agent_version"`
|
||||
CurrentAEVersion string `json:"current_ae_version"`
|
||||
AvailableAEVersions []string `json:"available_ae_versions"`
|
||||
RamGB *int `json:"ram_gb"`
|
||||
CPUCores *int `json:"cpu_cores"`
|
||||
CacheUsedGB *int `json:"cache_used_gb"`
|
||||
CachedTemplateMD5s []string `json:"cached_template_md5s"`
|
||||
}
|
||||
|
||||
type FrameProgressRequest struct {
|
||||
FrameJobID uuid.UUID `json:"frame_job_id" binding:"required"`
|
||||
FrameNumber int `json:"frame_number"`
|
||||
FileSizeBytes *int64 `json:"file_size_bytes"`
|
||||
CompletedAt *time.Time `json:"completed_at"`
|
||||
}
|
||||
|
||||
type CrashReportRequest struct {
|
||||
NodeID uuid.UUID `json:"node_id" binding:"required"`
|
||||
FrameJobID *uuid.UUID `json:"frame_job_id"`
|
||||
LastKnownFrame *int `json:"last_known_frame"`
|
||||
CrashSignal *string `json:"crash_signal"`
|
||||
AEVersion *string `json:"ae_version"`
|
||||
ErrorLogTail *string `json:"error_log_tail"`
|
||||
LogFileURL *string `json:"log_file_url"`
|
||||
}
|
||||
|
||||
type CacheUpdateRequest struct {
|
||||
Action string `json:"action" binding:"required"`
|
||||
ProjectID *uuid.UUID `json:"project_id"`
|
||||
AEPFileMD5 string `json:"aep_file_md5" binding:"required"`
|
||||
FileSizeBytes *int64 `json:"file_size_bytes"`
|
||||
CacheUsedGB *int `json:"cache_used_gb"`
|
||||
ErrorMessage *string `json:"error_message"`
|
||||
}
|
||||
|
||||
type ReplicaReadyRequest struct {
|
||||
NodeID uuid.UUID `json:"node_id" binding:"required"`
|
||||
ReplicaPath string `json:"replica_path" binding:"required"`
|
||||
ReplicaMD5 *string `json:"replica_md5"`
|
||||
}
|
||||
|
||||
type APIError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Claims carries JWT payload
|
||||
type Claims struct {
|
||||
UserID uuid.UUID `json:"sub"`
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
Reference in New Issue
Block a user