diff --git a/TASK.md b/TASK.md
index 116ee0d..5f9f0c5 100644
--- a/TASK.md
+++ b/TASK.md
@@ -1,716 +1,104 @@
-# TASK.md — Current Task: App Detail/Info Pages
+# TASK.md — Fix: App Info Page Bugs
> Read CLAUDE.md first for project context, workspace layout, and build instructions.
-> This file describes the current task to implement.
-## Overview
+## Problems
-Add a dedicated app information page to the felhom-controller dashboard. This page shows detailed
-info about each app (use cases, setup guide, prerequisites) and allows configuring **optional
-settings** (like API keys for metadata providers) both before and after deployment.
+Two bugs on the `/apps/romm` info page:
-The first app to get this treatment is **RoMM** (retro game ROM manager), which has optional
-metadata provider API keys (IGDB, SteamGridDB, ScreenScraper, MobyGames).
+### Bug 1: Logo SVG renders at native size (fills entire viewport)
-## Architecture context
+The `.app-info-logo` CSS sets `width: 80px; height: 80px` but the SVG logo image ignores these
+constraints and renders at its intrinsic size (hundreds of pixels wide/tall), pushing all content
+off-screen.
-- All HTML/CSS is embedded as Go string constants in `internal/web/templates.go`
-- All UI text is in **Hungarian**
-- Routes are defined in `internal/web/server.go` `ServeHTTP()` method (manual path matching)
-- API routes in `internal/api/router.go` `ServeHTTP()` method
-- App metadata lives in `.felhom.yml` files parsed by `internal/stacks/metadata.go`
-- Per-app deployed config is saved in `app.yaml` (managed by `internal/stacks/deploy.go`)
-- Template functions registered in `server.go` `initTemplates()`
+**Root cause:** SVG images inside `` tags can ignore `width`/`height` CSS if the SVG file
+itself has explicit `width`/`height` attributes or no `viewBox`. The `object-fit: contain` only
+works when the container actually constrains the image.
-## Changes needed
-
-This task touches **4 Go files** in the controller + **2 files** in the app-catalog repo.
-
----
-
-### 1. `controller/internal/stacks/metadata.go` — Add new structs
-
-Add these new fields to the existing `Metadata` struct, and add the new supporting structs:
-
-```go
-// Extend the existing Metadata struct with these new fields:
-type Metadata struct {
- // ... all existing fields stay unchanged ...
- DisplayName string `yaml:"display_name" json:"display_name"`
- Description string `yaml:"description" json:"description"`
- Category string `yaml:"category" json:"category"`
- Subdomain string `yaml:"subdomain" json:"subdomain"`
- Slug string `yaml:"slug" json:"slug"`
- Resources ResourceHints `yaml:"resources" json:"resources"`
- DeployFields []DeployField `yaml:"deploy_fields" json:"deploy_fields"`
-
- // NEW fields — add these:
- AppInfo AppInfo `yaml:"app_info" json:"app_info"`
- OptionalConfig []OptionalConfigGroup `yaml:"optional_config" json:"optional_config"`
-}
-
-// NEW: Detailed app information for the info page
-type AppInfo struct {
- Tagline string `yaml:"tagline" json:"tagline"`
- UseCases []string `yaml:"use_cases" json:"use_cases"`
- FirstSteps []string `yaml:"first_steps" json:"first_steps"`
- Prerequisites []string `yaml:"prerequisites" json:"prerequisites"`
- DefaultCreds string `yaml:"default_creds" json:"default_creds"`
- DocsURL string `yaml:"docs_url" json:"docs_url"`
-}
-
-// NEW: Optional config group (e.g., "Metadata providers")
-type OptionalConfigGroup struct {
- Group string `yaml:"group" json:"group"`
- Description string `yaml:"description" json:"description"`
- Fields []OptionalConfigField `yaml:"fields" json:"fields"`
-}
-
-// NEW: Individual optional config field
-type OptionalConfigField struct {
- EnvVar string `yaml:"env_var" json:"env_var"`
- Label string `yaml:"label" json:"label"`
- Type string `yaml:"type" json:"type"` // "text" or "secret_input"
- HelpURL string `yaml:"help_url" json:"help_url"`
- HelpText string `yaml:"help_text" json:"help_text"`
-}
-```
-
-Add helper methods:
-
-```go
-// HasAppInfo returns true if the metadata has any app info content.
-func (m *Metadata) HasAppInfo() bool {
- return m.AppInfo.Tagline != "" || len(m.AppInfo.UseCases) > 0 || len(m.AppInfo.FirstSteps) > 0
-}
-
-// HasOptionalConfig returns true if the metadata has any optional config groups.
-func (m *Metadata) HasOptionalConfig() bool {
- return len(m.OptionalConfig) > 0
-}
-```
-
-The existing `LoadMetadata()` function uses `yaml.Unmarshal` and will automatically populate the
-new fields if they exist in the YAML. No changes needed to `LoadMetadata()`.
-
----
-
-### 2. `controller/internal/stacks/deploy.go` — Add optional config update method
-
-Add a new method for updating optional env vars in `app.yaml`. This is safe to call on deployed
-apps — it only touches env vars listed in `optional_config`, never locked fields.
-
-```go
-// 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 {
- m.mu.Lock()
- defer m.mu.Unlock()
-
- stack, ok := m.stacks[stackName]
- if !ok {
- return fmt.Errorf("stack not found: %s", 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)
- appCfgPath := filepath.Join(m.cfg.Paths.StacksDir, stackName, "app.yaml")
- var appCfg AppConfig
- if data, err := os.ReadFile(appCfgPath); err == nil {
- _ = yaml.Unmarshal(data, &appCfg)
- }
- 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
- data, err := yaml.Marshal(&appCfg)
- if err != nil {
- return fmt.Errorf("marshal app.yaml: %w", err)
- }
- if err := os.WriteFile(appCfgPath, data, 0600); err != nil {
- return fmt.Errorf("write app.yaml: %w", err)
- }
- m.logger.Printf("[INFO] Saved updated app.yaml for %s", stackName)
-
- // If deployed, write .env and restart to pick up new env vars
- if stack.Deployed {
- // Write .env file — reuse the same pattern used by DeployStack
- // Look at how DeployStack writes the .env file and follow the same approach
- if err := m.writeEnvFile(stackName, appCfg.Env); err != nil {
- return fmt.Errorf("write .env: %w", err)
- }
- m.logger.Printf("[INFO] Restarting %s to apply new optional config", stackName)
- // Need to call the internal restart logic (without re-acquiring the lock)
- // Check if there's an unlocked restart method; if not, create one by factoring
- // the restart logic out of RestartStack into a restartStackLocked helper
- if err := m.restartStackLocked(stackName); err != nil {
- return fmt.Errorf("restart after config update: %w", err)
- }
- }
-
- return nil
-}
-```
-
-**IMPORTANT implementation notes:**
-- Check how `DeployStack` currently writes `.env` files — there's likely a helper that iterates
- `appCfg.Env` and writes `KEY=VALUE` lines. Reuse that exact logic.
-- The existing `RestartStack()` acquires the mutex lock. Since `UpdateOptionalConfig` already
- holds the lock, you need an internal unlocked version. Either:
- - Factor the restart logic out of `RestartStack` into `restartStackLocked` (recommended), or
- - Temporarily release and re-acquire the lock (less clean, avoid if possible)
-- Look at `manager.go` for the restart implementation to understand what needs to be factored out.
-
-Also add a public method to load app config (if not already public):
-
-```go
-// LoadAppConfig reads app.yaml from a stack directory. Returns nil if not found.
-func (m *Manager) LoadAppConfig(stackName string) (*AppConfig, error) {
- appCfgPath := filepath.Join(m.cfg.Paths.StacksDir, stackName, "app.yaml")
- data, err := os.ReadFile(appCfgPath)
- if err != nil {
- return nil, err
- }
- var cfg AppConfig
- if err := yaml.Unmarshal(data, &cfg); err != nil {
- return nil, err
- }
- return &cfg, nil
-}
-```
-
----
-
-### 3. `controller/internal/api/router.go` — Add optional config API endpoint
-
-Add a new route case in the `ServeHTTP` switch block:
-
-```go
-// POST /api/stacks/{name}/optional-config
-case hasSuffix(path, "/optional-config") && req.Method == http.MethodPost:
- r.updateOptionalConfig(w, req, extractName(path, "/optional-config"))
-```
-
-And the handler function:
-
-```go
-func (r *Router) updateOptionalConfig(w http.ResponseWriter, req *http.Request, name string) {
- r.logger.Printf("[API] Optional config update requested for stack: %s", name)
-
- var body struct {
- Values map[string]string `json:"values"`
- }
- if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
- writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "invalid request body"})
- return
- }
-
- if err := r.stackMgr.UpdateOptionalConfig(name, body.Values); err != nil {
- r.logger.Printf("[API] Optional config update failed for %s: %v", name, err)
- writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: err.Error()})
- return
- }
-
- writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "Beállítások frissítve"})
-}
-```
-
----
-
-### 4. `controller/internal/web/server.go` — Add info page route and handler
-
-#### 4a. The route already exists — update it
-
-There's already a case in `ServeHTTP` for `/apps/{slug}`. Currently it redirects to the deploy
-page. Replace the `appDetailHandler` method with a real page handler:
-
-```go
-func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug string) {
- var found *stacks.StackInfo
- for _, stack := range s.stackMgr.GetStacks() {
- if stack.Meta.Slug == slug {
- found = &stack
- break
- }
- }
- if found == nil {
- http.NotFound(w, r)
- return
- }
-
- // Load current optional config values from app.yaml
- currentValues := make(map[string]string)
- if appCfg, err := s.stackMgr.LoadAppConfig(found.Name); err == nil && appCfg != nil {
- for k, v := range appCfg.Env {
- currentValues[k] = v
- }
- }
-
- data := s.baseData("stacks", found.Meta.DisplayName)
- data["Stack"] = found
- data["Meta"] = found.Meta
- data["AppInfo"] = found.Meta.AppInfo
- data["OptionalConfig"] = found.Meta.OptionalConfig
- data["CurrentValues"] = currentValues
- data["HasAppInfo"] = found.Meta.HasAppInfo()
- data["HasOptionalConfig"] = found.Meta.HasOptionalConfig()
-
- s.render(w, "app_info", data)
-}
-```
-
-#### 4b. Add template functions
-
-In `initTemplates()`, add to the `funcMap`:
-
-```go
-"screenshotURL": func(slug string, index int) string {
- return s.cfg.AppScreenshotURL(slug, index)
-},
-"seq": func(n int) []int {
- result := make([]int, n)
- for i := range result {
- result[i] = i + 1
- }
- return result
-},
-```
-
----
-
-### 5. `controller/internal/web/templates.go` — Add info page template + CSS
-
-#### 5a. Update `allTemplates` const
-
-```go
-const allTemplates = layoutTmpl + dashboardTmpl + stacksTmpl + loginTmpl + logsTmpl + deployTmpl + appInfoTmpl
-```
-
-#### 5b. Add `appInfoTmpl` const
-
-Design goals: clean layout matching existing dark theme, Hungarian UI, sections for hero header,
-app info cards, screenshots, and optional config form with AJAX save.
-
-```go
-const appInfoTmpl = `
-{{define "app_info"}}
-{{template "layout_start" .}}
-
-
{{.AppInfo.Tagline}}
- {{else}} -{{.Meta.Description}}
- {{end}} - -{{.AppInfo.DefaultCreds}}
-⚠️ Az első bejelentkezés után azonnal változtasd meg!
-{{.Description}}
{{end}} - -{{.HelpText}}
{{end}} - {{if .HelpURL}}{{end}} - -` is empty in the DOM. + +This means `HasAppInfo()` returns false and `HasOptionalConfig()` returns false, which means the +`.felhom.yml` file on the demo node does NOT contain the new `app_info` and `optional_config` +sections. + +**Diagnosis steps (run these first):** + +```bash +# 1. Check if the stacks dir has the updated .felhom.yml +ssh kisfenyo@192.168.0.162 "cat /opt/docker/stacks/romm/.felhom.yml" +``` + +If the output does NOT contain `app_info:` and `optional_config:`, the catalog sync hasn't +picked up the changes. Check: + +```bash +# 2. Check if app-catalog repo was actually updated +cd /e/git/app-catalog-felhom.eu +git log --oneline -3 +cat templates/romm/.felhom.yml | grep -c "app_info" +``` + +If the local repo doesn't have `app_info` in the romm `.felhom.yml`, the previous session +didn't update the app-catalog repo. You need to: + +1. Update `E:\git\app-catalog-felhom.eu\templates\romm\.felhom.yml` with the full content + (see "RoMM .felhom.yml content" section below) +2. Update `E:\git\app-catalog-felhom.eu\templates\romm\docker-compose.yml` — add the missing + ScreenScraper + MobyGames env vars (see "RoMM docker-compose.yml changes" section below) +3. Commit and push the app-catalog repo +4. Trigger a catalog sync on the demo node (or wait 15 minutes) + +If the local repo DOES have `app_info` but the demo node doesn't, force a sync: + +```bash +# Trigger sync via API (need to login first to get session cookie) +# Or use the dashboard "Sablonok frissítése" button +``` + +**After the catalog sync, verify the fix:** + +```bash +ssh kisfenyo@192.168.0.162 "grep -c 'app_info' /opt/docker/stacks/romm/.felhom.yml" +# Should return 1 (meaning the app_info section exists) + +ssh kisfenyo@192.168.0.162 "grep -c 'optional_config' /opt/docker/stacks/romm/.felhom.yml" +# Should return 1 +``` + +Then reload `/apps/romm` in the browser — the info cards and optional config form should appear. + --- -### 6. Update navigation links in `templates.go` +## RoMM .felhom.yml content -#### 6a. Stack cards on Alkalmazások page → link to info page - -In `stacksTmpl`, the cards currently have `data-href="/stacks/{{.Name}}/deploy"`. -Change to: - -```html -{{if not .Protected}} data-href="/apps/{{.Meta.Slug}}"{{end}} -``` - -#### 6b. Stack cards on Dashboard → same change - -Check `dashboardTmpl` for any `data-href` attributes pointing to `/stacks/.../deploy` and update -them similarly to link to `/apps/{{.Meta.Slug}}` instead. - -#### 6c. Deploy page → add "Részletek" link - -In `deployTmpl`, add a link back to the info page near the top header area: - -```html -ℹ️ Részletek -``` - ---- - -### 7. RoMM `.felhom.yml` — Update in app-catalog repo - -File: `E:\git\app-catalog-felhom.eu\templates\romm\.felhom.yml` - -Replace the entire file with: +Replace `templates/romm/.felhom.yml` in the **app-catalog-felhom.eu** repo entirely with: ```yaml # ============================================================================= @@ -833,11 +221,10 @@ optional_config: --- -### 8. RoMM docker-compose.yml template — Add missing env vars +## RoMM docker-compose.yml changes -File: `E:\git\app-catalog-felhom.eu\templates\romm\docker-compose.yml` - -In the romm service's `environment:` section, after the existing STEAMGRIDDB line, add: +In `E:\git\app-catalog-felhom.eu\templates\romm\docker-compose.yml`, in the romm service's +`environment:` section, after the existing `STEAMGRIDDB_API_KEY` line, add these three lines: ```yaml - SCREENSCRAPER_USER=${SCREENSCRAPER_USER:-} @@ -849,86 +236,27 @@ In the romm service's `environment:` section, after the existing STEAMGRIDDB lin ## Implementation order -1. `metadata.go` — add structs + helper methods (safe, no side effects) -2. `deploy.go` — add `UpdateOptionalConfig` + `LoadAppConfig` + factor out restart logic -3. `router.go` — add `/optional-config` API route + handler -4. `templates.go` — add `appInfoTmpl`, CSS, update `allTemplates` -5. `server.go` — replace `appDetailHandler`, add template functions -6. `templates.go` — update navigation links (stack cards → `/apps/{slug}`, deploy page → "Részletek") -7. **Commit + push** the controller changes -8. **Build + push + deploy** the new controller image (see CLAUDE.md for exact commands) -9. **Verify** the controller starts correctly and the info page renders -10. Update RoMM `.felhom.yml` in app-catalog repo -11. Update RoMM `docker-compose.yml` in app-catalog repo -12. **Commit + push** the app-catalog changes -13. Trigger catalog sync on demo-felhom (or wait 15m) -14. **Verify** the RoMM info page shows all sections - ---- - -## Testing checklist - -- [ ] `/apps/romm` shows the info page with all sections -- [ ] Optional config fields display with current values (empty for fresh deployment) -- [ ] Saving optional config updates `app.yaml` and restarts the stack (if deployed) -- [ ] Saving optional config on a non-deployed app saves to `app.yaml` without error -- [ ] Screenshots section hidden gracefully if no assets exist -- [ ] Stack cards on Alkalmazások page link to `/apps/{slug}` -- [ ] Deploy page has "Részletek" link back to info page -- [ ] Apps without `app_info` show minimal info page (header + badges) -- [ ] Protected stacks (traefik, cloudflared, felhom-controller) not affected -- [ ] Build succeeds on build server -- [ ] Controller starts and runs without template parse errors -- [ ] New version shows in `docker ps` - ---- - -## Step 15: Update documentation (MANDATORY) - -After all code changes are done and verified, update these three files: - -### 15a. `CONTEXT.md` - -Add a new "What was just completed" section at the top of the history (push down the current -"What was just completed" to "Previously completed"). Include: - -- App info/detail pages feature added -- New `.felhom.yml` fields: `app_info`, `optional_config` -- New route: `GET /apps/{slug}` renders info page (was redirect to deploy) -- New API: `POST /api/stacks/{name}/optional-config` -- RoMM metadata updated with full app_info + 6 metadata provider optional config fields -- RoMM docker-compose.yml updated with ScreenScraper + MobyGames env vars -- Navigation: stack cards now link to info page, deploy page has "Részletek" link -- New controller version: v0.2.11 (or whatever was deployed) - -Update the "What's next" section — remove items that are done, add any new priorities. - -### 15b. `controller/README.md` - -Update to reflect: -- New `app_info` and `optional_config` sections in `.felhom.yml` format -- New info page route (`/apps/{slug}`) -- New API endpoint (`POST /api/stacks/{name}/optional-config`) -- Add to "What works" list: "App detail/info pages with optional config" - -### 15c. `CLAUDE.md` - -Add to the "Key patterns" section: -- App info pages at `/apps/{slug}` — detail view with use cases, setup guide, optional config -- Optional config saves to `app.yaml` and restarts deployed apps -- `optional_config` fields in `.felhom.yml` define post-deploy configurable env vars - -Add to "Important lessons learned" if any new lessons emerge during implementation. - ---- - -## Debugging tips - -- If template fails to parse, the controller crashes on startup with a `template.Must` panic. - The log output will show the exact parse error (usually missing closing brackets or undefined functions). -- If `app.yaml` already has deployed config, `UpdateOptionalConfig` merges new values without - touching locked fields. -- The `onerror` on screenshot images hides them if assets don't exist — graceful degradation. -- `config-input` uses `font-family: monospace` intentionally — API keys are easier to verify. -- The `{{index $.CurrentValues .EnvVar}}` in the template will return empty string for missing - keys (Go template `index` on map returns zero value) — no nil panic. \ No newline at end of file +1. **Diagnose Bug 2 first** — run the SSH commands above to check if `.felhom.yml` on the node + has the `app_info` section. This tells us whether the catalog update was missed or sync failed. +2. **Fix Bug 1** — update `.app-info-logo` CSS in `controller/internal/web/templates.go` +3. **Fix Bug 2** — if the app-catalog wasn't updated: + a. Update `templates/romm/.felhom.yml` in `E:\git\app-catalog-felhom.eu\` + b. Update `templates/romm/docker-compose.yml` in `E:\git\app-catalog-felhom.eu\` + c. Commit + push the app-catalog repo: + ```bash + cd /e/git/app-catalog-felhom.eu + git add -A && git commit -m "romm: add app_info + optional_config metadata, add ScreenScraper + MobyGames env vars" && git push + ``` +4. **Build + deploy** the controller with the CSS fix (follow CLAUDE.md build workflow) +5. **Trigger catalog sync** — either wait 15m or use the dashboard "Sablonok frissítése" button +6. **Verify** — reload `/apps/romm` and confirm: + - [ ] Logo is 80x80, not filling the screen + - [ ] Tagline text visible: "Retró játékgyűjtemény kezelő, böngésző és lejátszó" + - [ ] "Mire használható?" card with 5 use cases + - [ ] "Első lépések" card with 6 steps + - [ ] "Előfeltételek" card with 3 items + - [ ] "Alapértelmezett belépés" card showing "admin / admin" + - [ ] "Dokumentáció" card with link to RomM wiki + - [ ] "Opcionális beállítások" section with 6 metadata provider fields + - [ ] "Mentés" button on optional config form +7. **Update CONTEXT.md** with the fixes applied \ No newline at end of file