e66d4669d6
gates / gates (push) Failing after 47s
An app was six pages — details, install/settings, logs, export, the per-app backup panel and a
details card for moving data — and every "Vissza" on them went to the app LIST, so there was no way
back to the app you were looking at. It is now ONE page with four tab URLs under /apps/{slug}/,
one shared header and one set of banners.
settings Mukodes (Start/Restart/Stop, Stop confirms) + the cards that want something from the
household + the install-time and optional settings. The default for an installed app,
RENDERED at the bare URL and never redirected to.
data the data folders, move to another drive, the nightly copy, the 2nd-copy setting, export
guide tagline, screenshots, use cases, first steps, the catalog's guide, the FAQ, the docs
link — and the WHOLE page for an app that is not installed
logs the log, and the only tab whose poller runs
The tabs are links, not panels: a link in an alert has to land ON a tab, and the log poller must not
run on the other three. Start/Restart/Stop left the page header, where they read as menu buttons.
The old /stacks/{name}/{deploy,logs,export,backup} GETs answer 302 — never 301, which a browser
caches beyond anyone's reach — to the tab that now holds them, query string carried through, with
name->slug resolved through the stack list rather than assumed equal. Every POST on those paths is
untouched: the backup form, the app-email toggle and the reveal endpoints keep their URLs.
A value asked AFTER the app starts is now generic (D2). The catalog marks a field `after_start: true`
and supplies its label; the ACTION is Go, in a registry keyed by slug + env var. The catalog can
never declare what runs — a declared field with no registered action renders nothing, POSTs 404 and
logs one WARN with the env var KEY, never the value. A red-proof found the second half of that guard
unpinned: deleting the "the catalog must have DECLARED this field" check left the suite green, and a
POST for an undeclared field then reached Plex and opened the setup gate.
TestAfterStart_RegistryAloneIsNotAnInvitation pins it.
New catalog block app_info.guide (sections + faq + checked_version), parsed, localised field by
field and rendered with the household's own address filled in. It renders only when checked_version
names the image tag its author clicked through. No template carries one yet.
The Pi and "csak x86" badges are gone; PiCompatible and the catalog key stay this release, or a box
on 0.314.0 would read "csak x86" on every app (R-1067).
template_id_gate.py now follows {{template}} includes, with a scope decoy proving the walk did not
become vacuous.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
529 lines
20 KiB
Go
529 lines
20 KiB
Go
package stacks
|
|
|
|
import "strings"
|
|
|
|
// CATALOG COPY IN A SECOND LANGUAGE (localisation slice 5, R-560).
|
|
//
|
|
// A catalog template's customer-facing text is Hungarian in the fields the controller has always
|
|
// read (`description`, `app_info`, `deploy_fields[].label` …). English is a SIBLING BLOCK in the
|
|
// SAME file:
|
|
//
|
|
// description: "Titkosított jegyzet és szöveg megosztás"
|
|
// app_info:
|
|
// first_steps: [...]
|
|
// i18n:
|
|
// en:
|
|
// description: "Encrypted notes and text sharing"
|
|
// app_info:
|
|
// first_steps: [...]
|
|
//
|
|
// THREE PROPERTIES, EACH LOAD-BEARING.
|
|
//
|
|
// 1. **The Hungarian bytes never move.** A translation adds a block; it never edits a Hungarian
|
|
// string. `For("hu")` is the parsed struct with `I18n` cleared — pinned by
|
|
// TestMetaForHuIsIdentity over all 53 real catalog files.
|
|
//
|
|
// 2. **An older controller ignores the block.** `LoadMetadata` uses `yaml.Unmarshal`, which is
|
|
// NON-STRICT (no `KnownFields`; verified — this repo constructs no yaml.Decoder at all), so a
|
|
// controller built before this file silently drops `i18n:` and renders exactly as it did. That
|
|
// is what lets the catalog be pushed to the whole fleet ahead of the floor raise.
|
|
//
|
|
// 3. **Fallback is FIELD BY FIELD, never all-or-nothing.** A missing — or empty — English field
|
|
// shows the Hungarian one (10-localisation.md §4, operator ruling 3). A half-translated app is
|
|
// therefore a legal, shippable state, which is what makes the batch pushes safe.
|
|
//
|
|
// LISTS ARE REPLACED WHOLE, NEVER MERGED BY INDEX. `use_cases`, `first_steps` and `prerequisites`
|
|
// are prose in a running order; merging item 3 of one language with item 4 of another would produce
|
|
// a list nobody wrote. An English list that is present replaces the Hungarian list entirely; an
|
|
// absent or empty one leaves the Hungarian list alone.
|
|
//
|
|
// EVERY OTHER LIST IS MATCHED BY ITS OWN KEY, NEVER BY POSITION: `deploy_fields` by `env_var`,
|
|
// `options` by `value`, `optional_config` groups by `match_group` (the Hungarian `group` value they
|
|
// translate — a group has no other stable identity), `optional_config[].fields` by `env_var`,
|
|
// `integrations` by `target`, `data_paths` by `path`. Position matching would silently mistranslate
|
|
// the moment a Hungarian field is inserted above another, and nothing on the page would look wrong.
|
|
//
|
|
// WHAT AN OVERLAY MAY CARRY IS EXACTLY THE COPY FIELDS — no `env_var`, `type`, `default`,
|
|
// `generate`, `required`, `locked_after_deploy`, `data_key`, `path`, `role`, `docs_url`, no image,
|
|
// port or healthcheck. The struct shape below IS that rule: a translation cannot change what an app
|
|
// does, only what it says. The catalog's `check-copy-i18n.py` gate states the same rule on the file
|
|
// before it is ever parsed here.
|
|
|
|
// MetadataOverlay is one language's copy for one app. Every scalar is a POINTER so that "the
|
|
// translator did not write this field" is distinguishable from "the translator wrote an empty
|
|
// string" — both fall back to Hungarian, but only the first is silent.
|
|
type MetadataOverlay struct {
|
|
Description *string `yaml:"description,omitempty"`
|
|
AppInfo *AppInfoOverlay `yaml:"app_info,omitempty"`
|
|
DeployFields []DeployFieldOverlay `yaml:"deploy_fields,omitempty"`
|
|
OptionalConfig []OptionalConfigGroupOverlay `yaml:"optional_config,omitempty"`
|
|
Integrations []IntegrationOverlay `yaml:"integrations,omitempty"`
|
|
DataPaths []DataPathOverlay `yaml:"data_paths,omitempty"`
|
|
InitialCreds *InitialCredentialsOverlay `yaml:"initial_credentials,omitempty"`
|
|
}
|
|
|
|
// AppInfoOverlay carries the info page's prose. The three lists replace whole.
|
|
type AppInfoOverlay struct {
|
|
Tagline *string `yaml:"tagline,omitempty"`
|
|
UseCases []string `yaml:"use_cases,omitempty"`
|
|
FirstSteps []string `yaml:"first_steps,omitempty"`
|
|
Prerequisites []string `yaml:"prerequisites,omitempty"`
|
|
DefaultCreds *string `yaml:"default_creds,omitempty"`
|
|
AddPeople *string `yaml:"add_people,omitempty"`
|
|
Guide *AppGuideOverlay `yaml:"guide,omitempty"`
|
|
}
|
|
|
|
// AppGuideOverlay translates the user guide. `checked_version` is NOT here and that is the rule
|
|
// stated in code: a version vouches for a click-through, and a translator did not do it.
|
|
//
|
|
// SECTIONS AND FAQ ENTRIES ARE MATCHED BY INDEX, and they are the only lists in this file that are.
|
|
// The header above says every list is matched by its own key — a guide section has no key to match
|
|
// on. Its title is the very thing being translated, so a `match_title` would have to be kept in step
|
|
// with the Hungarian by hand, which is the drift the key rule exists to prevent. Index matching is
|
|
// safe HERE because the catalog's copy gate requires a translated guide to carry the same number of
|
|
// sections and FAQ entries in the same running order, and because the fallback is field by field:
|
|
// a misaligned entry shows the Hungarian one, never a sentence nobody wrote. An English list LONGER
|
|
// than the Hungarian one is ignored past the Hungarian count, with one WARN naming the slug
|
|
// (GuideOverlayExtras).
|
|
type AppGuideOverlay struct {
|
|
Sections []GuideSectionOverlay `yaml:"sections,omitempty"`
|
|
FAQ []GuideFAQOverlay `yaml:"faq,omitempty"`
|
|
}
|
|
|
|
// GuideSectionOverlay translates one section, found by its POSITION. Steps replace whole, like
|
|
// every other prose list in this file.
|
|
type GuideSectionOverlay struct {
|
|
Title *string `yaml:"title,omitempty"`
|
|
Intro *string `yaml:"intro,omitempty"`
|
|
Steps []string `yaml:"steps,omitempty"`
|
|
}
|
|
|
|
// GuideFAQOverlay translates one FAQ entry, found by its POSITION.
|
|
type GuideFAQOverlay struct {
|
|
Q *string `yaml:"q,omitempty"`
|
|
A *string `yaml:"a,omitempty"`
|
|
}
|
|
|
|
// DeployFieldOverlay translates one deploy field, found by EnvVar.
|
|
type DeployFieldOverlay struct {
|
|
EnvVar string `yaml:"env_var"`
|
|
Label *string `yaml:"label,omitempty"`
|
|
Description *string `yaml:"description,omitempty"`
|
|
Placeholder *string `yaml:"placeholder,omitempty"`
|
|
Options []SelectOptionOverlay `yaml:"options,omitempty"`
|
|
}
|
|
|
|
// SelectOptionOverlay translates one select option's label, found by Value.
|
|
type SelectOptionOverlay struct {
|
|
Value string `yaml:"value"`
|
|
Label *string `yaml:"label,omitempty"`
|
|
}
|
|
|
|
// OptionalConfigGroupOverlay translates one optional-config group. MatchGroup is the HUNGARIAN
|
|
// `group:` value it belongs to — a group carries no id, and its own label is the thing being
|
|
// translated, so the match key has to be stated rather than derived.
|
|
type OptionalConfigGroupOverlay struct {
|
|
MatchGroup string `yaml:"match_group"`
|
|
Group *string `yaml:"group,omitempty"`
|
|
Description *string `yaml:"description,omitempty"`
|
|
Fields []OptionalConfigFieldOverlay `yaml:"fields,omitempty"`
|
|
}
|
|
|
|
// OptionalConfigFieldOverlay translates one optional-config field, found by EnvVar.
|
|
type OptionalConfigFieldOverlay struct {
|
|
EnvVar string `yaml:"env_var"`
|
|
Label *string `yaml:"label,omitempty"`
|
|
HelpText *string `yaml:"help_text,omitempty"`
|
|
}
|
|
|
|
// IntegrationOverlay translates one integration offer, found by Target.
|
|
type IntegrationOverlay struct {
|
|
Target string `yaml:"target"`
|
|
Label *string `yaml:"label,omitempty"`
|
|
Description *string `yaml:"description,omitempty"`
|
|
}
|
|
|
|
// DataPathOverlay translates one data-folder label, found by Path.
|
|
type DataPathOverlay struct {
|
|
Path string `yaml:"path"`
|
|
Label *string `yaml:"label,omitempty"`
|
|
}
|
|
|
|
// InitialCredentialsOverlay translates the note shown beside a first-login credential. Only the
|
|
// note: the file path, the format and the keys are configuration, and the credential VALUE never
|
|
// passes through here at all.
|
|
type InitialCredentialsOverlay struct {
|
|
Note *string `yaml:"note,omitempty"`
|
|
}
|
|
|
|
// BaseLanguage is the language the unmarshalled fields themselves are written in. It is not a
|
|
// choice a template can make — the catalog's Hungarian is the base by construction (the freeze
|
|
// gate in the catalog repo pins it byte for byte).
|
|
const BaseLanguage = "hu"
|
|
|
|
// overlayStr returns the translated string, or the base one when the translation is absent or
|
|
// blank. A blank translation is treated as ABSENT and never renders an empty box — operator
|
|
// ruling 3, 10-localisation.md §4.
|
|
func overlayStr(p *string, base string) string {
|
|
if p == nil || strings.TrimSpace(*p) == "" {
|
|
return base
|
|
}
|
|
return *p
|
|
}
|
|
|
|
// overlayList returns the translated list, or the base one when the translation is absent or
|
|
// empty. Whole-list replacement, never a per-index merge — see the header.
|
|
func overlayList(in []string, base []string) []string {
|
|
if len(in) == 0 {
|
|
return base
|
|
}
|
|
out := make([]string, len(in))
|
|
copy(out, in)
|
|
return out
|
|
}
|
|
|
|
// overlayGuide returns the translated guide. `CheckedVersion` is carried through from the base
|
|
// UNTOUCHED: it is the render gate, not copy, so a translation can neither set it nor clear it — an
|
|
// untranslated guide and a translated one are shown or hidden by the same fact.
|
|
//
|
|
// Entries are matched by index and every field falls back on its own (see AppGuideOverlay). Nothing
|
|
// is mutated: both slices are rebuilt.
|
|
func overlayGuide(ov *AppGuideOverlay, base AppGuide) AppGuide {
|
|
if ov == nil {
|
|
return base
|
|
}
|
|
out := base
|
|
if len(ov.Sections) > 0 && len(base.Sections) > 0 {
|
|
secs := make([]GuideSection, len(base.Sections))
|
|
copy(secs, base.Sections)
|
|
for i := range secs {
|
|
if i >= len(ov.Sections) {
|
|
break
|
|
}
|
|
so := ov.Sections[i]
|
|
secs[i].Title = overlayStr(so.Title, secs[i].Title)
|
|
secs[i].Intro = overlayStr(so.Intro, secs[i].Intro)
|
|
secs[i].Steps = overlayList(so.Steps, secs[i].Steps)
|
|
}
|
|
out.Sections = secs
|
|
}
|
|
if len(ov.FAQ) > 0 && len(base.FAQ) > 0 {
|
|
faq := make([]GuideFAQ, len(base.FAQ))
|
|
copy(faq, base.FAQ)
|
|
for i := range faq {
|
|
if i >= len(ov.FAQ) {
|
|
break
|
|
}
|
|
qo := ov.FAQ[i]
|
|
faq[i].Q = overlayStr(qo.Q, faq[i].Q)
|
|
faq[i].A = overlayStr(qo.A, faq[i].A)
|
|
}
|
|
out.FAQ = faq
|
|
}
|
|
return out
|
|
}
|
|
|
|
// GuideOverlayExtras counts the guide entries a language's overlay carries BEYOND the Hungarian
|
|
// count — the ones overlayGuide ignores. It exists so the page that renders the guide can say so
|
|
// once in a log line: `For` runs per request and holds no logger, and a silently dropped
|
|
// translation is exactly the kind of absence nobody notices. Zero for the base language, for a
|
|
// language with no block, and for a correctly sized overlay.
|
|
func (m Metadata) GuideOverlayExtras(lang string) (sections, faq int) {
|
|
if lang == "" || lang == BaseLanguage {
|
|
return 0, 0
|
|
}
|
|
ov, ok := m.I18n[lang]
|
|
if !ok || ov.AppInfo == nil || ov.AppInfo.Guide == nil {
|
|
return 0, 0
|
|
}
|
|
if n := len(ov.AppInfo.Guide.Sections) - len(m.AppInfo.Guide.Sections); n > 0 {
|
|
sections = n
|
|
}
|
|
if n := len(ov.AppInfo.Guide.FAQ) - len(m.AppInfo.Guide.FAQ); n > 0 {
|
|
faq = n
|
|
}
|
|
return sections, faq
|
|
}
|
|
|
|
// For returns this app's metadata as the given language renders it.
|
|
//
|
|
// For the base language — and for any language this template has no block for — it is the parsed
|
|
// struct with `I18n` cleared, so a Hungarian page cannot be changed by the presence of a
|
|
// translation. For a language with a block, copy fields are replaced one by one and EVERYTHING
|
|
// ELSE is carried through untouched.
|
|
//
|
|
// It NEVER mutates the receiver. Every slice it changes is copied first: `Metadata` values live
|
|
// inside the stack manager's cache and are handed to many requests at once, so an in-place edit
|
|
// would leak one household's language into another's page.
|
|
//
|
|
// VALUE receiver, like CanInstall and CatalogSinceAge above it — see the comment there: a pointer
|
|
// receiver on a type that reaches html/template inside an interface{} is a render-time 500.
|
|
func (m Metadata) For(lang string) Metadata {
|
|
out := m
|
|
out.I18n = nil
|
|
if lang == "" || lang == BaseLanguage {
|
|
return out
|
|
}
|
|
ov, ok := m.I18n[lang]
|
|
if !ok {
|
|
return out
|
|
}
|
|
|
|
out.Description = overlayStr(ov.Description, out.Description)
|
|
|
|
if ov.AppInfo != nil {
|
|
out.AppInfo.Tagline = overlayStr(ov.AppInfo.Tagline, out.AppInfo.Tagline)
|
|
out.AppInfo.DefaultCreds = overlayStr(ov.AppInfo.DefaultCreds, out.AppInfo.DefaultCreds)
|
|
out.AppInfo.AddPeople = overlayStr(ov.AppInfo.AddPeople, out.AppInfo.AddPeople)
|
|
out.AppInfo.UseCases = overlayList(ov.AppInfo.UseCases, out.AppInfo.UseCases)
|
|
out.AppInfo.FirstSteps = overlayList(ov.AppInfo.FirstSteps, out.AppInfo.FirstSteps)
|
|
out.AppInfo.Prerequisites = overlayList(ov.AppInfo.Prerequisites, out.AppInfo.Prerequisites)
|
|
out.AppInfo.Guide = overlayGuide(ov.AppInfo.Guide, out.AppInfo.Guide)
|
|
}
|
|
|
|
if len(ov.DeployFields) > 0 && len(out.DeployFields) > 0 {
|
|
fields := make([]DeployField, len(out.DeployFields))
|
|
copy(fields, out.DeployFields)
|
|
for _, fo := range ov.DeployFields {
|
|
for i := range fields {
|
|
if fields[i].EnvVar != fo.EnvVar || fo.EnvVar == "" {
|
|
continue
|
|
}
|
|
fields[i].Label = overlayStr(fo.Label, fields[i].Label)
|
|
fields[i].Description = overlayStr(fo.Description, fields[i].Description)
|
|
fields[i].Placeholder = overlayStr(fo.Placeholder, fields[i].Placeholder)
|
|
if len(fo.Options) > 0 && len(fields[i].Options) > 0 {
|
|
opts := make([]SelectOption, len(fields[i].Options))
|
|
copy(opts, fields[i].Options)
|
|
for _, oo := range fo.Options {
|
|
for j := range opts {
|
|
if opts[j].Value == oo.Value && oo.Value != "" {
|
|
opts[j].Label = overlayStr(oo.Label, opts[j].Label)
|
|
}
|
|
}
|
|
}
|
|
fields[i].Options = opts
|
|
}
|
|
break
|
|
}
|
|
}
|
|
out.DeployFields = fields
|
|
}
|
|
|
|
if len(ov.OptionalConfig) > 0 && len(out.OptionalConfig) > 0 {
|
|
groups := make([]OptionalConfigGroup, len(out.OptionalConfig))
|
|
copy(groups, out.OptionalConfig)
|
|
for _, gov := range ov.OptionalConfig {
|
|
for i := range groups {
|
|
if groups[i].Group != gov.MatchGroup || gov.MatchGroup == "" {
|
|
continue
|
|
}
|
|
groups[i].Description = overlayStr(gov.Description, groups[i].Description)
|
|
if len(gov.Fields) > 0 && len(groups[i].Fields) > 0 {
|
|
flds := make([]OptionalConfigField, len(groups[i].Fields))
|
|
copy(flds, groups[i].Fields)
|
|
for _, fo := range gov.Fields {
|
|
for j := range flds {
|
|
if flds[j].EnvVar == fo.EnvVar && fo.EnvVar != "" {
|
|
flds[j].Label = overlayStr(fo.Label, flds[j].Label)
|
|
flds[j].HelpText = overlayStr(fo.HelpText, flds[j].HelpText)
|
|
}
|
|
}
|
|
}
|
|
groups[i].Fields = flds
|
|
}
|
|
// Group LAST: it is the match key, so translating it earlier would make the
|
|
// remaining overlay entries for this group unmatchable.
|
|
groups[i].Group = overlayStr(gov.Group, groups[i].Group)
|
|
break
|
|
}
|
|
}
|
|
out.OptionalConfig = groups
|
|
}
|
|
|
|
if len(ov.Integrations) > 0 && len(out.Integrations) > 0 {
|
|
ints := make([]IntegrationDef, len(out.Integrations))
|
|
copy(ints, out.Integrations)
|
|
for _, io := range ov.Integrations {
|
|
for i := range ints {
|
|
if ints[i].Target == io.Target && io.Target != "" {
|
|
ints[i].Label = overlayStr(io.Label, ints[i].Label)
|
|
ints[i].Description = overlayStr(io.Description, ints[i].Description)
|
|
}
|
|
}
|
|
}
|
|
out.Integrations = ints
|
|
}
|
|
|
|
if len(ov.DataPaths) > 0 && len(out.DataPaths) > 0 {
|
|
dps := make([]DataPath, len(out.DataPaths))
|
|
copy(dps, out.DataPaths)
|
|
for _, do := range ov.DataPaths {
|
|
for i := range dps {
|
|
if dps[i].Path == do.Path && do.Path != "" {
|
|
dps[i].Label = overlayStr(do.Label, dps[i].Label)
|
|
}
|
|
}
|
|
}
|
|
out.DataPaths = dps
|
|
}
|
|
|
|
if ov.InitialCreds != nil && out.InitialCreds != nil {
|
|
ic := *out.InitialCreds
|
|
ic.Note = overlayStr(ov.InitialCreds.Note, ic.Note)
|
|
out.InitialCreds = &ic
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// MetaFor is For on a stack, and it is the ONLY way a page should reach catalog copy: reading
|
|
// `stack.Meta.Description` straight out of the manager renders Hungarian to an English household.
|
|
// TestNoDirectMetaCopyReadOnPages pins that rule on the handler sources.
|
|
func (s Stack) MetaFor(lang string) Metadata { return s.Meta.For(lang) }
|
|
|
|
// LocalizeStacks returns the list as the given language renders it.
|
|
//
|
|
// For the base language it returns the INPUT SLICE ITSELF — not a copy. That is deliberate and it
|
|
// is the parity guarantee in code: the Hungarian page cannot differ from the pre-change one,
|
|
// because nothing on its path was touched. For another language every element is copied (Stack is
|
|
// a value type) and only its Meta replaced.
|
|
func LocalizeStacks(in []Stack, lang string) []Stack {
|
|
if lang == "" || lang == BaseLanguage {
|
|
return in
|
|
}
|
|
out := make([]Stack, len(in))
|
|
copy(out, in)
|
|
for i := range out {
|
|
out[i].Meta = out[i].Meta.For(lang)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// LocalizeStack is LocalizeStacks for a single stack.
|
|
func LocalizeStack(in Stack, lang string) Stack {
|
|
in.Meta = in.Meta.For(lang)
|
|
return in
|
|
}
|
|
|
|
// LocalizeStackPtr is LocalizeStack for a stack POINTER, returning a NEW pointer so the value the
|
|
// caller holds — which may be the manager's own — is never written through. A nil stack stays nil.
|
|
func LocalizeStackPtr(in *Stack, lang string) *Stack {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := *in
|
|
out.Meta = out.Meta.For(lang)
|
|
return &out
|
|
}
|
|
|
|
// cloneStrPtr returns a fresh pointer to a copy of *p, or nil.
|
|
func cloneStrPtr(p *string) *string {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
v := *p
|
|
return &v
|
|
}
|
|
|
|
// cloneStrs returns a fresh copy of in, keeping nil as nil and empty as empty.
|
|
func cloneStrs(in []string) []string {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := make([]string, len(in))
|
|
copy(out, in)
|
|
return out
|
|
}
|
|
|
|
// Clone returns a deep copy of the overlay: no pointer, slice or nested struct is shared with the
|
|
// receiver. It exists for deepCopyStack (R-591) — a Stack copy is "a snapshot the caller may
|
|
// mutate", and the overlay must be one too. Pinned by TestDeepCopyStackI18nIsNotShared.
|
|
func (o MetadataOverlay) Clone() MetadataOverlay {
|
|
c := MetadataOverlay{
|
|
Description: cloneStrPtr(o.Description),
|
|
}
|
|
if o.AppInfo != nil {
|
|
ai := AppInfoOverlay{
|
|
Tagline: cloneStrPtr(o.AppInfo.Tagline),
|
|
UseCases: cloneStrs(o.AppInfo.UseCases),
|
|
FirstSteps: cloneStrs(o.AppInfo.FirstSteps),
|
|
Prerequisites: cloneStrs(o.AppInfo.Prerequisites),
|
|
DefaultCreds: cloneStrPtr(o.AppInfo.DefaultCreds),
|
|
AddPeople: cloneStrPtr(o.AppInfo.AddPeople),
|
|
}
|
|
if o.AppInfo.Guide != nil {
|
|
g := AppGuideOverlay{}
|
|
if o.AppInfo.Guide.Sections != nil {
|
|
g.Sections = make([]GuideSectionOverlay, len(o.AppInfo.Guide.Sections))
|
|
for i, se := range o.AppInfo.Guide.Sections {
|
|
g.Sections[i] = GuideSectionOverlay{
|
|
Title: cloneStrPtr(se.Title),
|
|
Intro: cloneStrPtr(se.Intro),
|
|
Steps: cloneStrs(se.Steps),
|
|
}
|
|
}
|
|
}
|
|
if o.AppInfo.Guide.FAQ != nil {
|
|
g.FAQ = make([]GuideFAQOverlay, len(o.AppInfo.Guide.FAQ))
|
|
for i, qa := range o.AppInfo.Guide.FAQ {
|
|
g.FAQ[i] = GuideFAQOverlay{Q: cloneStrPtr(qa.Q), A: cloneStrPtr(qa.A)}
|
|
}
|
|
}
|
|
ai.Guide = &g
|
|
}
|
|
c.AppInfo = &ai
|
|
}
|
|
if o.DeployFields != nil {
|
|
c.DeployFields = make([]DeployFieldOverlay, len(o.DeployFields))
|
|
for i, f := range o.DeployFields {
|
|
nf := DeployFieldOverlay{
|
|
EnvVar: f.EnvVar,
|
|
Label: cloneStrPtr(f.Label),
|
|
Description: cloneStrPtr(f.Description),
|
|
Placeholder: cloneStrPtr(f.Placeholder),
|
|
}
|
|
if f.Options != nil {
|
|
nf.Options = make([]SelectOptionOverlay, len(f.Options))
|
|
for j, op := range f.Options {
|
|
nf.Options[j] = SelectOptionOverlay{Value: op.Value, Label: cloneStrPtr(op.Label)}
|
|
}
|
|
}
|
|
c.DeployFields[i] = nf
|
|
}
|
|
}
|
|
if o.OptionalConfig != nil {
|
|
c.OptionalConfig = make([]OptionalConfigGroupOverlay, len(o.OptionalConfig))
|
|
for i, g := range o.OptionalConfig {
|
|
ng := OptionalConfigGroupOverlay{
|
|
MatchGroup: g.MatchGroup,
|
|
Group: cloneStrPtr(g.Group),
|
|
Description: cloneStrPtr(g.Description),
|
|
}
|
|
if g.Fields != nil {
|
|
ng.Fields = make([]OptionalConfigFieldOverlay, len(g.Fields))
|
|
for j, f := range g.Fields {
|
|
ng.Fields[j] = OptionalConfigFieldOverlay{EnvVar: f.EnvVar, Label: cloneStrPtr(f.Label), HelpText: cloneStrPtr(f.HelpText)}
|
|
}
|
|
}
|
|
c.OptionalConfig[i] = ng
|
|
}
|
|
}
|
|
if o.Integrations != nil {
|
|
c.Integrations = make([]IntegrationOverlay, len(o.Integrations))
|
|
for i, in := range o.Integrations {
|
|
c.Integrations[i] = IntegrationOverlay{Target: in.Target, Label: cloneStrPtr(in.Label), Description: cloneStrPtr(in.Description)}
|
|
}
|
|
}
|
|
if o.DataPaths != nil {
|
|
c.DataPaths = make([]DataPathOverlay, len(o.DataPaths))
|
|
for i, d := range o.DataPaths {
|
|
c.DataPaths[i] = DataPathOverlay{Path: d.Path, Label: cloneStrPtr(d.Label)}
|
|
}
|
|
}
|
|
if o.InitialCreds != nil {
|
|
c.InitialCreds = &InitialCredentialsOverlay{Note: cloneStrPtr(o.InitialCreds.Note)}
|
|
}
|
|
return c
|
|
}
|