Add app detail/info pages with optional config support

- New route: GET /apps/{slug} renders info page with use cases, setup guide, prerequisites
- New API: POST /api/stacks/{name}/optional-config for updating optional env vars
- New structs: AppInfo, OptionalConfigGroup, OptionalConfigField in metadata.go
- UpdateOptionalConfig saves to app.yaml and restarts deployed stacks with new env vars
- Info page template with hero section, screenshots, info cards, optional config form
- Navigation: stack cards now link to /apps/{slug}, deploy page has "Részletek" link

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 20:12:18 +01:00
parent 528f64cab7
commit 6080170367
5 changed files with 544 additions and 19 deletions
+79
View File
@@ -282,6 +282,85 @@ func (m *Manager) GetDeployFields(name string) (*Metadata, *AppConfig, error) {
return &meta, appCfg, nil
}
// UpdateOptionalConfig updates optional env vars in app.yaml and restarts the stack if deployed.
// Only updates env vars that are listed in the metadata's optional_config sections.
func (m *Manager) UpdateOptionalConfig(stackName string, values map[string]string) error {
stack, ok := m.GetStack(stackName)
if !ok {
return fmt.Errorf("stack %q not found", stackName)
}
// Build a set of allowed env vars from optional_config
allowed := make(map[string]bool)
for _, group := range stack.Meta.OptionalConfig {
for _, field := range group.Fields {
allowed[field.EnvVar] = true
}
}
if len(allowed) == 0 {
return fmt.Errorf("no optional config fields defined for %s", stackName)
}
// Load existing app.yaml (or create empty one)
stackDir := filepath.Dir(stack.ComposePath)
appCfg := LoadAppConfig(stackDir)
if appCfg == nil {
appCfg = &AppConfig{
Env: make(map[string]string),
}
}
if appCfg.Env == nil {
appCfg.Env = make(map[string]string)
}
// Update only allowed env vars
changed := false
for key, val := range values {
if !allowed[key] {
m.logger.Printf("[WARN] Ignoring non-optional env var: %s", key)
continue
}
if appCfg.Env[key] != val {
appCfg.Env[key] = val
changed = true
m.logger.Printf("[INFO] Updated optional config %s for %s", key, stackName)
}
}
if !changed {
return nil
}
// Save app.yaml
if err := SaveAppConfig(stackDir, appCfg); err != nil {
return fmt.Errorf("saving app config: %w", err)
}
m.logger.Printf("[INFO] Saved updated app.yaml for %s", stackName)
// If deployed, recreate containers to pick up new env vars
// (docker compose restart does NOT pick up new env vars — must use up -d)
if stack.Deployed {
m.logger.Printf("[INFO] Restarting %s to apply new optional config", stackName)
env := m.stackEnv(stackDir)
if _, err := m.composeExecCustomEnv(stackDir, env, "up", "-d"); err != nil {
return fmt.Errorf("restart after config update: %w", err)
}
m.logPostStartStatus(stackName, stackDir, env)
}
return m.RefreshStatus()
}
// LoadAppConfigByName reads app.yaml for a named stack. Returns nil if not found.
func (m *Manager) LoadAppConfigByName(stackName string) *AppConfig {
stack, ok := m.GetStack(stackName)
if !ok {
return nil
}
stackDir := filepath.Dir(stack.ComposePath)
return LoadAppConfig(stackDir)
}
// --- App config persistence ---
func LoadAppConfig(stackDir string) *AppConfig {