diff --git a/CHANGELOG.md b/CHANGELOG.md index 52bd341..4691b16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,11 @@ Not released yet (bake → vouch → floor, `09` §3 decision 247). Design: felh - **„Kész" never opens an unclaimed Plex (R-1045)** — the controller asks Plex itself (`/identity` with the JSON Accept header) both after the claim and on the household's press. The live template carries NO probe: a controller before 0.313.0 would send no Accept header and refuse the press forever. `TestPlexDonePress_RefusedWhileUnclaimed` (red-proved). +- **`after_start:` deploy fields (R-1028/R-1045; a background security review of catalog `0188430`)** — this controller + does not ask such a field in the wizard and accepts it empty (Plex's claim code is asked on the Plex page after the + start); an older controller ignores the key and keeps the field required. The first catalog push had made the code + optional for EVERY controller, which let an old controller's „Kész" open an unclaimed Plex. `TestAfterStartField_*` + (red-proved). - **Old Sonarr/Radarr installs repaired by themselves (R-1047)** — only when /media is no longer bound: same-path root folder, `moveFiles:false` editor, rescan, old root removed, Radarr's collections too. `TestRepairOldRoot_K7` (red-proved: moveFiles=true is caught). diff --git a/controller/internal/stacks/deploy.go b/controller/internal/stacks/deploy.go index eb29f84..d48164a 100644 --- a/controller/internal/stacks/deploy.go +++ b/controller/internal/stacks/deploy.go @@ -390,8 +390,8 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) { } } - // Validate required fields - if field.Required && value == "" { + // Validate required fields (an after_start field is asked after the start — R-1028) + if field.Required && value == "" && !field.AfterStart { clearDeploying() return "", util.MsgErrorf(ErrRequiredField, "err.stacks.field_required", field.Label, field.EnvVar) } diff --git a/controller/internal/stacks/media_library_test.go b/controller/internal/stacks/media_library_test.go index 36edbfb..0499ac2 100644 --- a/controller/internal/stacks/media_library_test.go +++ b/controller/internal/stacks/media_library_test.go @@ -1,6 +1,8 @@ package stacks import ( + "gopkg.in/yaml.v3" + "os" "path/filepath" "strings" @@ -169,3 +171,19 @@ func TestR1031_MediaAppIsNotAskedForADrive(t *testing.T) { t.Fatal("an app without the marker must still be asked for its drive") } } + +// R-1028/R-1045: an `after_start` field (Plex's claim code) is not in this controller's wizard and may be empty here; +// the same template stays REQUIRED for an older controller (it ignores the key). COMPANION RED-PROOF: drop the +// AfterStart skip in UserFacingFields → this test fails. Restored. +func TestAfterStartField_NotAskedAtInstall(t *testing.T) { + m := Metadata{DeployFields: []DeployField{{EnvVar: "PLEX_CLAIM", Type: "text", Required: true, AfterStart: true}, {EnvVar: "SUBDOMAIN", Type: "subdomain"}}} + for _, f := range m.UserFacingFields() { + if f.EnvVar == "PLEX_CLAIM" { + t.Fatal("the claim code is asked at install — the 4-minute race is back (R-1028)") + } + } + var meta Metadata + if err := yaml.Unmarshal([]byte("deploy_fields:\n - env_var: PLEX_CLAIM\n type: text\n required: true\n after_start: true\n"), &meta); err != nil || !meta.DeployFields[0].AfterStart || !meta.DeployFields[0].Required { + t.Fatalf("the template shape must parse as required + after_start: %+v %v", meta.DeployFields, err) + } +} diff --git a/controller/internal/stacks/metadata.go b/controller/internal/stacks/metadata.go index 9d51a44..1f1da01 100644 --- a/controller/internal/stacks/metadata.go +++ b/controller/internal/stacks/metadata.go @@ -195,16 +195,21 @@ type ResourceHints struct { // DeployField defines one configuration field shown during first deployment. type DeployField struct { - EnvVar string `yaml:"env_var" json:"env_var"` - Label string `yaml:"label" json:"label"` - Type string `yaml:"type" json:"type"` // domain, subdomain, secret, password, path, text, select, boolean - Generate string `yaml:"generate" json:"generate"` // e.g., "password:24", "hex:32", "static:admin" - Default string `yaml:"default" json:"default"` - Required bool `yaml:"required" json:"required"` - Placeholder string `yaml:"placeholder" json:"placeholder"` - Description string `yaml:"description" json:"description"` - LockedAfterDeploy bool `yaml:"locked_after_deploy" json:"locked_after_deploy"` - Options []SelectOption `yaml:"options" json:"options,omitempty"` + EnvVar string `yaml:"env_var" json:"env_var"` + Label string `yaml:"label" json:"label"` + Type string `yaml:"type" json:"type"` // domain, subdomain, secret, password, path, text, select, boolean + Generate string `yaml:"generate" json:"generate"` // e.g., "password:24", "hex:32", "static:admin" + Default string `yaml:"default" json:"default"` + Required bool `yaml:"required" json:"required"` + Placeholder string `yaml:"placeholder" json:"placeholder"` + Description string `yaml:"description" json:"description"` + LockedAfterDeploy bool `yaml:"locked_after_deploy" json:"locked_after_deploy"` + // AfterStart (v0.313.0, R-1028/R-1045): this controller asks for the value AFTER the app started (Plex's claim code on + // the Plex page), so the wizard does not show the field and an empty value is allowed. An OLDER controller ignores the + // key and keeps the field — required — so a template can stay safe for both (a code made optional for everyone would + // let an old controller's „Kész" open an unclaimed Plex). Pinned by TestAfterStartField_*. + AfterStart bool `yaml:"after_start,omitempty" json:"after_start,omitempty"` + Options []SelectOption `yaml:"options" json:"options,omitempty"` // DataKey marks a field as a DATA-ENCRYPTING key (e.g. AdventureLog's "Titkosítási kulcs"): // the app encrypts stored data with it, so regenerating it would render restored data // unreadable. It is a fail-closed annotation only — the recovery unit never stores secrets; @@ -539,6 +544,9 @@ func (m *Metadata) UserFacingFields() []DeployField { if m.MediaLibrary != nil && f.EnvVar == "HDD_PATH" { continue } + if f.AfterStart { + continue + } if f.Type != "domain" && f.Type != "secret" { fields = append(fields, f) }