739b67856a
Build / build (push) Successful in 10m39s
The clean-break delete that closes the workload-first refactor arc.
Net diff: ~30 backend files deleted, ~20 modified, ~12k LOC removed
on the Go side; entire /projects /stacks /sites /deploy frontend
trees gone; ~6.7k LOC removed on the Svelte/TypeScript side.
Backend
- API handlers gone: internal/api/{projects,stages,stage_env,stacks,
static_sites,deploys,instances,volume_browser}.go
- Store CRUD + tests gone: internal/store/{projects,stages,stage_env,
stacks,static_sites,static_site_secrets,deploys,poll_state,volumes,
workload_sync}.go (+ _test.go siblings)
- Legacy deployer pipeline gone: internal/deployer/{bluegreen,promote,
rollback,subdomain,resolver_test}.go; deployer.go trimmed to just the
dispatch surface used by the plugin pipeline
- internal/staticsite/{manager,healthcheck}.go and
internal/stack/manager.go gone (the rest of those packages stay as
helpers imported by the static + compose plugins)
- internal/registry/poller.go gone (legacy registry poller)
- internal/volume.ResolvePath gone; ResolveWorkloadPath stays
- internal/webhook: handleWebhook (project) + handleSiteWebhook (site)
gone; only POST /api/webhook/triggers/{secret} remains
- workload-side webhook URL handlers (getWorkloadWebhook +
regenerateWorkloadWebhook + EnsureWorkloadWebhookSecret +
SetWorkloadWebhookSecret + GetWorkloadByWebhookSecret) gone — they
minted URLs that would 404 against the new trigger-only ingress
- cmd/server/main.go: dropped staticsite.Manager, stack.Manager,
staticsite.HealthChecker, registry poller, SetSiteSyncTriggerer,
SetStaticSiteManager, SetStackManager, wireStaticBackend
- store/store.go: idempotent DROP TABLE IF EXISTS for every legacy
table (projects, stages, stage_env, volumes, deploys, deploy_logs,
poll_states, stacks, stack_revisions, stack_deploys, static_sites,
static_site_secrets); FK order children-then-parents
- store/models.go: dropped Project, Stage, Deploy, DeployLog, StageEnv,
Volume, StaticSite, StaticSiteSecret, Stack, StackRevision,
StackDeploy types; kept WorkloadKind constants as documented strings
- internal/store/helpers.go (new): BoolToInt, rowScanner,
GenerateWebhookSecret extracted from deleted CRUD files
- internal/api/secrets.go (new): forwards to store.GenerateWebhookSecret
so api + store paths share one secret-generation impl (no
panic-vs-UUID-fallback divergence)
- internal/reconciler/reconciler.go: dropped legacy stack-by-compose
+ static-site label paths; only canonical tinyforge.workload.id
dispatch remains
- providers (gitea_content/github_provider/gitlab_provider) gained
path-traversal rejection on every tree entry
- internal/webhook ParsedImage / ParseImageRef demoted to package-
private (no external callers)
Frontend
- /projects /stacks /sites /deploy routes deleted (entire trees)
- ProjectCard / InstanceCard / StaleContainerCard components deleted
- api.ts: dropped every project/stage/stack/site/deploy/instance
helper + types (Project, Stage, Stack, StaticSite, Deploy,
Instance, Volume, etc.); kept Workload, Container, App, Settings,
Registry, EventTrigger, LogScanRule, webhook envelopes
- WorkloadWebhook type + getWorkloadWebhook/regenerateWorkloadWebhook
api functions gone (mirror of the backend deletion above)
- web/src/routes/+layout.svelte: dropped /projects /sites /stacks
/deploy nav entries, trimmed quick-nav keymap
- web/src/routes/+page.svelte: dashboard rewrite — reads
listWorkloads + listContainers only; 4-card stat grid
(workloads/running/failed/stale) + recent workloads strip
- navCounts.ts, SystemHealthCard.svelte, ContainerLogs.svelte,
ContainerStats.svelte, StatusBadge.svelte, TagCombobox.svelte,
proxies/+page.svelte, containers/+page.svelte all rewired to the
workload-first surface
- AbortController plumbing on dashboard, nav-counts, stale page,
SystemHealthCard so navigation doesn't leave dangling fetches
- i18n: dropped projects.*, projectDetail.*, envEditor.*,
volumeEditor.*, volumeBrowser.*, quickDeploy.*, sites.*, stacks.*,
instance.*, confirm.* namespaces; en/ru parity preserved (1042
keys each)
Hardening from go-reviewer + security-reviewer + typescript-reviewer
subagent passes (0 CRITICAL across all three; 1 HIGH + ~12 MEDIUM
addressed inline before commit):
- Sec H1: dead-end workload webhook URL handlers (would mint URLs
that 404 the new trigger-only ingress) deleted across backend +
frontend
- Go M1: IsTerminalDeployStatus dropped (no production callers)
- Go M2: ParsedImage/ParseImageRef lowercased (in-package only)
- Go M6: generateWebhookSecret unified — api shim forwards to
store.GenerateWebhookSecret
- Doc/comment freshness: stage_id (no longer FK), ProxyRoute legacy
field names, workloadIDRow rationale, webhook_deliveries.target_type
enum, WebhookDeliveryLog component header
Doc
- WORKLOAD_REFACTOR_TODO: cutover marked DONE; all three Priority 1
items are now shipped. Next focus is Priority 3 polish (apps.* i18n
+ codemap entries) and Priority 4 tests.
Behavioral notes for operators upgrading from a pre-cutover build
- Existing rows in the dropped tables disappear on first boot.
- Legacy webhook URLs at /api/webhook/{secret} and
/api/webhook/sites/{secret} return 404; CI configs must repoint to
/api/webhook/triggers/{secret} (the trigger-split boot backfill
lifted any embedded workload secret onto a Trigger row, so the
secret value itself carries over).
- Frontend routes /projects /stacks /sites /deploy are gone; nav
links replaced with /apps and /triggers.
399 lines
18 KiB
Go
399 lines
18 KiB
Go
package store
|
|
|
|
// Registry represents a container image registry.
|
|
type Registry struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
URL string `json:"url"`
|
|
Type string `json:"type"`
|
|
Token string `json:"token"`
|
|
Owner string `json:"owner"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// Settings holds global application configuration (single-row pattern).
|
|
type Settings struct {
|
|
Domain string `json:"domain"`
|
|
ServerIP string `json:"server_ip"` // Docker host IP (for NPM remote forwarding)
|
|
PublicIP string `json:"public_ip"` // Public-facing IP for DNS A records (e.g., NPM/proxy host)
|
|
Network string `json:"network"`
|
|
SubdomainPattern string `json:"subdomain_pattern"`
|
|
NotificationURL string `json:"notification_url"`
|
|
NotificationSecret string `json:"-"` // outgoing-webhook signing secret; never serialized directly
|
|
NpmURL string `json:"npm_url"`
|
|
NpmEmail string `json:"npm_email"`
|
|
NpmPassword string `json:"npm_password"`
|
|
PollingInterval string `json:"polling_interval"`
|
|
BaseVolumePath string `json:"base_volume_path"`
|
|
SSLCertificateID int `json:"ssl_certificate_id"`
|
|
StaleThresholdDays int `json:"stale_threshold_days"`
|
|
AllowedVolumePaths string `json:"allowed_volume_paths"` // JSON array of allowed absolute paths
|
|
WildcardDNS bool `json:"wildcard_dns"`
|
|
DNSProvider string `json:"dns_provider"`
|
|
CloudflareAPIToken string `json:"cloudflare_api_token"`
|
|
CloudflareZoneID string `json:"cloudflare_zone_id"`
|
|
NpmRemote bool `json:"npm_remote"`
|
|
NpmAccessListID int `json:"npm_access_list_id"`
|
|
ProxyProvider string `json:"proxy_provider"`
|
|
TraefikEntrypoint string `json:"traefik_entrypoint"`
|
|
TraefikCertResolver string `json:"traefik_cert_resolver"`
|
|
TraefikNetwork string `json:"traefik_network"`
|
|
TraefikAPIURL string `json:"traefik_api_url"`
|
|
ImagePruneThresholdMB int `json:"image_prune_threshold_mb"`
|
|
BackupEnabled bool `json:"backup_enabled"`
|
|
BackupIntervalHours int `json:"backup_interval_hours"`
|
|
BackupRetentionCount int `json:"backup_retention_count"`
|
|
// AutoBackupBeforeDeploy creates a "pre-deploy" Tinyforge DB backup
|
|
// at the start of every project deploy. Independent of BackupEnabled
|
|
// (which governs the periodic auto-backup cron).
|
|
AutoBackupBeforeDeploy bool `json:"auto_backup_before_deploy"`
|
|
StatsIntervalSeconds int `json:"stats_interval_seconds"` // 0 disables collection
|
|
StatsRetentionHours int `json:"stats_retention_hours"` // 0 disables collection
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// ContainerStatsSample is one persisted sample of container resource usage.
|
|
// Cumulative counters (network, block I/O) require differencing two samples
|
|
// to get rates; CPU is already a percent-since-previous-sample value.
|
|
type ContainerStatsSample struct {
|
|
ContainerID string `json:"container_id"`
|
|
OwnerType string `json:"owner_type"` // "instance" or "site"
|
|
OwnerID string `json:"owner_id"`
|
|
TS int64 `json:"ts"` // Unix seconds UTC
|
|
CPUPercent float64 `json:"cpu_percent"`
|
|
MemoryUsage int64 `json:"memory_usage"`
|
|
MemoryLimit int64 `json:"memory_limit"`
|
|
NetworkRxBytes int64 `json:"network_rx_bytes"`
|
|
NetworkTxBytes int64 `json:"network_tx_bytes"`
|
|
BlockReadBytes int64 `json:"block_read_bytes"`
|
|
BlockWriteBytes int64 `json:"block_write_bytes"`
|
|
}
|
|
|
|
// SystemStatsSample is one persisted host-level snapshot that aggregates
|
|
// workload usage across all containers plus daemon capacity + disk totals.
|
|
type SystemStatsSample struct {
|
|
TS int64 `json:"ts"` // Unix seconds UTC
|
|
NCPU int `json:"ncpu"`
|
|
MemoryTotal int64 `json:"memory_total"`
|
|
WorkloadCPUPercent float64 `json:"workload_cpu_percent"`
|
|
WorkloadMemUsage int64 `json:"workload_mem_usage"`
|
|
ContainersRunning int `json:"containers_running"`
|
|
DiskTotalBytes int64 `json:"disk_total_bytes"`
|
|
}
|
|
|
|
// Backup represents a backup metadata record.
|
|
type Backup struct {
|
|
ID string `json:"id"`
|
|
Filename string `json:"filename"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
BackupType string `json:"backup_type"` // "manual" or "auto"
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
// DNSRecord tracks a DNS record managed by the application.
|
|
type DNSRecord struct {
|
|
ID string `json:"id"`
|
|
FQDN string `json:"fqdn"`
|
|
ProviderRecordID string `json:"provider_record_id"`
|
|
ConsumerType string `json:"consumer_type"` // "instance" or "standalone"
|
|
ConsumerID string `json:"consumer_id"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// ProxyRoute shapes one proxy-enabled container row for the Proxies
|
|
// page. The legacy field names (ProjectID, ProjectName, StageID,
|
|
// StageName, InstanceID) are retained verbatim for the existing
|
|
// frontend contract — after the workload-first cutover they map to:
|
|
// ProjectID/Name → workload id / workload name
|
|
// StageID/Name → containers.stage_id / containers.role
|
|
// InstanceID → container row id
|
|
// Source → "instance" for image/compose, "static_site" for static
|
|
// Renaming would require a coordinated frontend change; deferred.
|
|
type ProxyRoute struct {
|
|
Source string `json:"source"`
|
|
InstanceID string `json:"instance_id"`
|
|
ProjectID string `json:"project_id"`
|
|
ProjectName string `json:"project_name"`
|
|
StageID string `json:"stage_id"`
|
|
StageName string `json:"stage_name"`
|
|
ImageTag string `json:"image_tag"`
|
|
Subdomain string `json:"subdomain"`
|
|
Domain string `json:"domain"`
|
|
ContainerID string `json:"container_id"`
|
|
Port int `json:"port"`
|
|
ProxyRouteID string `json:"proxy_route_id"`
|
|
NpmProxyID int `json:"npm_proxy_id"`
|
|
Status string `json:"status"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
// WorkloadVolume is the plugin-shape equivalent of legacy Volume: a
|
|
// per-workload mount declaration. The Scope enum matches the existing
|
|
// VolumeScope contract so the legacy resolver can be reused once its
|
|
// project_id assumption is loosened.
|
|
type WorkloadVolume struct {
|
|
ID string `json:"id"`
|
|
WorkloadID string `json:"workload_id"`
|
|
Source string `json:"source"`
|
|
Target string `json:"target"`
|
|
Scope string `json:"scope"`
|
|
Name string `json:"name"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// WorkloadEnv is the plugin-shape equivalent of StageEnv: per-workload
|
|
// environment variable overrides, optionally encrypted at rest. Read by
|
|
// the Source plugin at deploy time, merged on top of source_config.env.
|
|
type WorkloadEnv struct {
|
|
ID string `json:"id"`
|
|
WorkloadID string `json:"workload_id"`
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
Encrypted bool `json:"encrypted"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// VolumeScope defines the sharing scope for a volume mount.
|
|
// Valid scopes: instance, stage, project, project_named, named, ephemeral.
|
|
type VolumeScope string
|
|
|
|
const (
|
|
VolumeScopeInstance VolumeScope = "instance"
|
|
VolumeScopeStage VolumeScope = "stage"
|
|
VolumeScopeProject VolumeScope = "project"
|
|
VolumeScopeProjectNamed VolumeScope = "project_named"
|
|
VolumeScopeNamed VolumeScope = "named"
|
|
VolumeScopeEphemeral VolumeScope = "ephemeral"
|
|
VolumeScopeAbsolute VolumeScope = "absolute"
|
|
)
|
|
|
|
// ValidVolumeScopes contains all valid scope values for validation.
|
|
var ValidVolumeScopes = []VolumeScope{
|
|
VolumeScopeInstance, VolumeScopeStage, VolumeScopeProject,
|
|
VolumeScopeProjectNamed, VolumeScopeNamed, VolumeScopeEphemeral,
|
|
VolumeScopeAbsolute,
|
|
}
|
|
|
|
// IsValidVolumeScope returns true if the given string is a valid scope.
|
|
func IsValidVolumeScope(s string) bool {
|
|
for _, v := range ValidVolumeScopes {
|
|
if string(v) == s {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// EventLog represents a persistent event log entry.
|
|
type EventLog struct {
|
|
ID int64 `json:"id"`
|
|
Source string `json:"source"`
|
|
Severity string `json:"severity"` // info, warn, error
|
|
Message string `json:"message"`
|
|
Metadata string `json:"metadata"` // JSON-encoded structured data
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
// EventTrigger is a filter+action rule evaluated against EventLog
|
|
// entries published on the bus. When all non-empty filters match, the
|
|
// trigger fires its configured action (webhook today, additional action
|
|
// types extensible via the ActionType enum).
|
|
//
|
|
// Filter fields use a comma-separated list shape for multi-value
|
|
// filters (severity, source) to keep the schema flat — empty string
|
|
// means "no filter on this dimension." FilterMessageRegex is a single
|
|
// regex evaluated against EventLog.Message.
|
|
//
|
|
// Loop-prevention: deliveries are recorded in webhook_deliveries (the
|
|
// existing audit trail). The dispatcher MUST NOT write to event_log
|
|
// or it will recurse.
|
|
type EventTrigger struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
FilterSeverity string `json:"filter_severity"` // comma list: "warn,error"; "" = any
|
|
FilterSource string `json:"filter_source"` // comma list: "logscan,deploy"; "" = any
|
|
FilterMessageRegex string `json:"filter_message_regex"` // "" = any
|
|
ActionType string `json:"action_type"` // "webhook" today
|
|
ActionTarget string `json:"action_target"` // URL for webhook
|
|
ActionSecret string `json:"action_secret"` // optional HMAC secret for signed delivery
|
|
Enabled bool `json:"enabled"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// EventTriggerActionType enumerates the supported action_type values.
|
|
// Adding a new action is additive — old triggers keep working, the
|
|
// dispatcher just learns a new branch.
|
|
const (
|
|
EventTriggerActionWebhook = "webhook"
|
|
)
|
|
|
|
// LogScanRule is one regex-based pattern the log scanner evaluates
|
|
// against container log lines. The (workload_id, overrides_id) pair
|
|
// implements the "global rule with optional per-workload override"
|
|
// pattern documented in docs/LOGSCAN_AND_TRIGGERS_TODO.md:
|
|
//
|
|
// - WorkloadID == "" && OverridesID == 0 → global rule, applies to
|
|
// every workload unless overridden.
|
|
// - WorkloadID != "" && OverridesID == 0 → workload-only addition.
|
|
// - WorkloadID != "" && OverridesID != 0 → override of the named
|
|
// global rule for one workload (Enabled=false to disable globally
|
|
// for this workload).
|
|
type LogScanRule struct {
|
|
ID int64 `json:"id"`
|
|
WorkloadID string `json:"workload_id"` // "" = global
|
|
OverridesID int64 `json:"overrides_id"` // 0 = not an override
|
|
Name string `json:"name"`
|
|
Pattern string `json:"pattern"` // regex, compiled at load
|
|
Severity string `json:"severity"` // info|warn|error
|
|
Streams string `json:"streams"` // all|stdout|stderr
|
|
CooldownSeconds int `json:"cooldown_seconds"`
|
|
Enabled bool `json:"enabled"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// Log scan stream filter values. "all" reads both streams; "stdout"
|
|
// or "stderr" filter to one. Used both for store validation and at
|
|
// docker-side log read time.
|
|
const (
|
|
LogScanStreamAll = "all"
|
|
LogScanStreamStdout = "stdout"
|
|
LogScanStreamStderr = "stderr"
|
|
)
|
|
|
|
// Log scan severity values mirror the event_log enum so a matched
|
|
// rule lands as an event_log row with the rule's severity verbatim.
|
|
const (
|
|
LogScanSeverityInfo = "info"
|
|
LogScanSeverityWarn = "warn"
|
|
LogScanSeverityError = "error"
|
|
)
|
|
|
|
// WorkloadKind enumerates the legacy discriminator values written into
|
|
// containers.workload_kind and workloads.kind. After the hard cutover the
|
|
// backing project / stack / static_site tables are gone — these constants
|
|
// are just strings used to filter the unified containers index in the UI.
|
|
type WorkloadKind string
|
|
|
|
const (
|
|
WorkloadKindProject WorkloadKind = "project"
|
|
WorkloadKindStack WorkloadKind = "stack"
|
|
WorkloadKindSite WorkloadKind = "site"
|
|
)
|
|
|
|
// Workload is the unifying primitive that abstracts Project, Stack, and StaticSite.
|
|
// Each row is paired with exactly one project/stack/site via (Kind, RefID).
|
|
// Notification + webhook config moves here so it lives in one place across kinds.
|
|
//
|
|
// SourceKind / SourceConfig / TriggerKind / TriggerConfig / PublicFaces /
|
|
// ParentWorkloadID populate the unified plugin model from the Workload-first
|
|
// refactor. Existing rows keep these empty until they are explicitly migrated
|
|
// or replaced — the legacy Kind/RefID columns continue to point at
|
|
// project/stack/site rows in parallel during the cutover.
|
|
type Workload struct {
|
|
ID string `json:"id"`
|
|
Kind string `json:"kind"` // project | stack | site (legacy discriminator)
|
|
RefID string `json:"ref_id"`
|
|
Name string `json:"name"`
|
|
AppID string `json:"app_id"` // nullable; "" = unassigned (a.k.a. GroupID after rename)
|
|
SourceKind string `json:"source_kind"` // "" until plugin-mode populated
|
|
SourceConfig string `json:"source_config"` // JSON-encoded, decoded by the matching Source
|
|
TriggerKind string `json:"trigger_kind"`
|
|
TriggerConfig string `json:"trigger_config"` // JSON-encoded, decoded by the matching Trigger
|
|
PublicFaces string `json:"public_faces"` // JSON-encoded []PublicFace
|
|
ParentWorkloadID string `json:"parent_workload_id"` // "" = root; non-empty = stage chain
|
|
NotificationURL string `json:"notification_url"`
|
|
NotificationSecret string `json:"-"` // never serialized
|
|
WebhookSecret string `json:"-"` // URL-identifier secret; never serialized
|
|
WebhookSigningSecret string `json:"-"` // HMAC key; never serialized
|
|
WebhookRequireSignature bool `json:"webhook_require_signature"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// Container is the normalized index of every Tinyforge-managed container.
|
|
// Replaces the project-specific Instance table after migration. Subdomain/
|
|
// proxy fields are hoisted as first-class columns because ListProxyRoutes,
|
|
// stale detection, and dashboard queries filter on them frequently.
|
|
//
|
|
// StageID is populated by the deployer for project containers so ListProxyRoutes
|
|
// survives stage renames; it stays empty for stack and site rows.
|
|
type Container struct {
|
|
ID string `json:"id"`
|
|
WorkloadID string `json:"workload_id"`
|
|
WorkloadKind string `json:"workload_kind"` // denormalized for filtered queries
|
|
Role string `json:"role"` // stage name (project), service name (stack), '' (site)
|
|
StageID string `json:"stage_id"` // project containers only; '' otherwise
|
|
ContainerID string `json:"container_id"` // Docker container ID; '' between create+start
|
|
ImageRef string `json:"image_ref"` // "image:tag" as scheduled
|
|
ImageTag string `json:"image_tag"` // just the tag, for ListProxyRoutes
|
|
Host string `json:"host"`
|
|
State string `json:"state"` // running | stopped | failed | removing | missing
|
|
Port int `json:"port"`
|
|
Subdomain string `json:"subdomain"`
|
|
ProxyRouteID string `json:"proxy_route_id"`
|
|
NpmProxyID int `json:"npm_proxy_id"`
|
|
LastSeenAt string `json:"last_seen_at"`
|
|
// ExtraJSON carries source-specific metadata that isn't promoted to a
|
|
// first-class column — currently per-face proxy route IDs for
|
|
// multi-face image deploys. Stored as a JSON object; '{}' on empty
|
|
// rows. Sources own the shape; consumers should tolerate unknown
|
|
// keys.
|
|
ExtraJSON string `json:"extra_json"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// Trigger is a first-class redeploy signal source. Triggers were embedded
|
|
// in workload rows (workload.trigger_kind / trigger_config) until the
|
|
// trigger-split refactor; they are now standalone records bound to
|
|
// workloads via WorkloadTriggerBinding so a single trigger (a webhook,
|
|
// registry watcher, schedule, git push) can fan out to many workloads.
|
|
//
|
|
// Webhook secrets live here, not on the workload — the inbound webhook
|
|
// URL identifies a trigger, which then resolves its bindings to decide
|
|
// which workloads to fire.
|
|
type Trigger struct {
|
|
ID string `json:"id"`
|
|
Kind string `json:"kind"` // registry | git | manual | schedule | log_scan | ...
|
|
Name string `json:"name"` // human-readable, unique
|
|
Config string `json:"config"` // JSON-encoded, decoded by the matching plugin
|
|
WebhookSecret string `json:"-"` // URL-identifier secret; never serialized
|
|
WebhookSigningSecret string `json:"-"` // HMAC key; never serialized
|
|
WebhookRequireSignature bool `json:"webhook_require_signature"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// WorkloadTriggerBinding joins a Workload to a Trigger. BindingConfig is
|
|
// the per-binding override applied on top of Trigger.Config (top-level
|
|
// JSON merge: binding fields win). Empty BindingConfig means "use the
|
|
// trigger's config verbatim". Enabled false skips the binding without
|
|
// deleting it (useful for paused stages).
|
|
type WorkloadTriggerBinding struct {
|
|
ID string `json:"id"`
|
|
WorkloadID string `json:"workload_id"`
|
|
TriggerID string `json:"trigger_id"`
|
|
BindingConfig string `json:"binding_config"` // JSON-encoded; "{}" = none
|
|
Enabled bool `json:"enabled"`
|
|
SortOrder int `json:"sort_order"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// App is an optional grouping of workloads (e.g., "my-saas" = web project + worker stack + redis stack).
|
|
// Schema lives here from day one so future UI work is unblocked, but no UI is wired in v1.
|
|
type App struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|