diff --git a/CLAUDE.md b/CLAUDE.md index 6a08d9f..be0dadf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,6 +12,7 @@ Compose stacks on customer hardware via a Hungarian-language web dashboard. See `controller/README.md` for full architecture and status. See `CONTEXT.md` for current project state, recent work, and decisions (update after each session). +See `TASK.md` for the current task to implement (if it exists). Claude in Chrome extension is available — can be used to test web UI on demo-felhom.eu or verify dashboard deployments in browser. @@ -22,37 +23,49 @@ Claude in Chrome extension is available — can be used to test web UI on demo-f - Add debug capabilities (logging, verbose output) for easier troubleshooting - If you need more input or troubleshooting command output, ask first — don't guess -## Repository structure +## Workspace layout -This repo has two main parts: +Claude Code runs on Windows. The working directory is `E:\git\` (mapped as `/e/git/` in Git Bash). +This repo is at: ``` -deploy-felhom-compose/ -├── controller/ # Go application (this is the main codebase) -│ ├── cmd/controller/ # Entry point (main.go) +E:\git\deploy-felhom-compose\ (or /e/git/deploy-felhom-compose/ in Git Bash) +├── controller/ # Go application (main codebase) +│ ├── cmd/controller/ # Entry point (main.go) │ ├── internal/ -│ │ ├── config/ # YAML config loading -│ │ ├── stacks/ # Docker Compose operations, deploy flow -│ │ ├── sync/ # Git sync — periodic pull of app catalog repo -│ │ ├── api/ # REST API endpoints -│ │ ├── system/ # System info (memory, disk) -│ │ └── web/ # Dashboard UI (embedded HTML/CSS templates) +│ │ ├── config/ # YAML config loading +│ │ ├── stacks/ # Docker Compose operations, deploy flow +│ │ ├── sync/ # Git sync — periodic pull of app catalog repo +│ │ ├── api/ # REST API endpoints +│ │ ├── system/ # System info (memory, disk) +│ │ └── web/ # Dashboard UI (embedded HTML/CSS templates) │ ├── Dockerfile │ ├── Makefile │ └── go.mod -├── scripts/ # Setup scripts for customer nodes -├── CLAUDE.md # This file -└── CONTEXT.md # Project memory / state +├── scripts/ # Setup scripts for customer nodes +├── CLAUDE.md # This file +├── CONTEXT.md # Project memory / state +└── TASK.md # Current task (if exists) ``` -## Related repositories (not in this repo) +Related repos (same parent directory): +``` +E:\git\app-catalog-felhom.eu\ # Docker Compose templates + .felhom.yml metadata per app +E:\git\felhom.eu\ # Website (htmls) + k3s manifests +E:\git\homelab-manifests\ # k3s cluster manifests (dooplex.hu services) +E:\git\misc-scripts\ # Helper scripts +``` -- **app-catalog-felhom.eu** — Docker Compose templates + .felhom.yml metadata per app -- **felhom.eu** — Website (htmls) + k3s manifests for the web server -- **homelab-manifests** — Viktor's k3s cluster manifests (dooplex.hu services) -- **misc-scripts** — Helper scripts for daily tasks +All repos hosted at `gitea.dooplex.hu/admin/`. Git credentials are stored (`git config credential.helper store`). -All hosted at `gitea.dooplex.hu/admin/` +## SSH access + +SSH key-based authentication is configured and working. No password prompts. + +| Host | IP | User | Role | +|------|----|------|------| +| Build server | 192.168.0.180 | kisfenyo | Build + push container images | +| Demo node | 192.168.0.162 | kisfenyo | Test deployment (demo-felhom.eu) | ## Test environments @@ -64,20 +77,73 @@ All hosted at `gitea.dooplex.hu/admin/` - Pi-hole DNS on local network forwards `*.demo-felhom.eu` → 192.168.0.162 - External access via Cloudflare Tunnel → Traefik reverse proxy -## Build & deploy workflow +## Build & deploy workflow — MANDATORY + +After making code changes to the controller, you **MUST** build, push, and deploy the new image. +Do NOT leave code changes uncommitted or undeployed. The full cycle is: + +### Step 1: Commit and push changes -Code is edited locally on Windows. Build happens on the server via SSH. ```bash -# Push changes -git add -A && git commit -m "..." && git push - -# Build + push image on server -ssh kisfenyo@192.168.0.180 "cd ~/build/felhom-controller && git -C ~/git/deploy-felhom-compose pull && ./build.sh --push" - -# Deploy on demo node -ssh kisfenyo@192.168.0.162 "cd /opt/docker/felhom-controller && docker pull gitea.dooplex.hu/admin/felhom-controller: && docker compose up -d" +cd /e/git/deploy-felhom-compose +git add -A && git commit -m "" && git push ``` +### Step 2: Build + push the container image on the build server + +The build server (192.168.0.180) has the build toolchain. The version tag should be incremented +from the current running version. + +First, check the current running version: +```bash +ssh kisfenyo@192.168.0.162 "docker ps --filter name=felhom-controller --format '{{.Image}}'" +``` + +Then build with the next version (e.g., if current is 0.2.10, use 0.2.11): +```bash +ssh kisfenyo@192.168.0.180 "cd ~/build/felhom-controller && git -C ~/git/deploy-felhom-compose pull && ./build.sh --push" +``` + +The build script: +- Pulls latest code from Gitea +- Builds a multi-arch Docker image (amd64 + arm64) if `--multiarch`, or current arch if `--push` +- Pushes to `gitea.dooplex.hu/admin/felhom-controller:` +- Expects the version as first argument (e.g., `0.2.11`) + +### Step 3: Deploy on the demo node + +```bash +ssh kisfenyo@192.168.0.162 "cd /opt/docker/felhom-controller && docker pull gitea.dooplex.hu/admin/felhom-controller: && sed -i 's|felhom-controller:[^ ]*|felhom-controller:|' docker-compose.yml && docker compose up -d" +``` + +### Step 4: Verify the deployment + +```bash +ssh kisfenyo@192.168.0.162 "docker ps --filter name=felhom-controller --format '{{.Image}} {{.Status}}'" +``` + +Should show the new version and "Up" status. Also check logs for startup errors: +```bash +ssh kisfenyo@192.168.0.162 "docker logs felhom-controller --tail 20" +``` + +### Build workflow summary + +| Step | Command | Where | +|------|---------|-------| +| 1. Commit + push | `git add -A && git commit -m "..." && git push` | Local (this repo) | +| 2. Build + push image | `ssh 192.168.0.180 "... ./build.sh --push"` | Build server | +| 3. Deploy | `ssh 192.168.0.162 "... docker compose up -d"` | Demo node | +| 4. Verify | `ssh 192.168.0.162 "docker ps ..."` | Demo node | + +**IMPORTANT:** If you make changes to the app-catalog-felhom.eu repo, commit and push those too: +```bash +cd /e/git/app-catalog-felhom.eu +git add -A && git commit -m "" && git push +``` +The controller's git sync will pick up catalog changes within 15 minutes, or you can trigger it +manually via the dashboard "Sablonok frissítése" button. + ## Tech stack - **Language:** Go 1.22+ @@ -143,4 +209,14 @@ Key patterns used in `internal/stacks/`: 5. In-memory `Deployed` flag must be set BEFORE `docker compose up -d` (not after) — compose can take 30-60s for image pulls; revert both in-memory and disk on failure 6. `docker compose up -d` returns exit 0 even when containers crash-loop — post-start status check is essential for detecting failures 7. Mealie image has no wget/curl — use Python TCP socket check for healthcheck; set `start_period: 60s` for DB migration time -8. Always verify container images have the healthcheck tool (`wget`, `curl`, etc.) before using it — Alpine has BusyBox wget, Python images have `python3` \ No newline at end of file +8. Always verify container images have the healthcheck tool (`wget`, `curl`, etc.) before using it — Alpine has BusyBox wget, Python images have `python3` + +## End-of-session checklist + +Before ending a session, always: + +1. **Commit and push** all code changes +2. **Build, push, and deploy** the new controller image (if controller code changed) +3. **Update CONTEXT.md** with what was done, decisions made, and what's next +4. **Update controller/README.md** if architecture or features changed +5. **Verify** the deployment is working (check `docker ps` and logs) \ No newline at end of file diff --git a/TASK.md b/TASK.md new file mode 100644 index 0000000..116ee0d --- /dev/null +++ b/TASK.md @@ -0,0 +1,934 @@ +# TASK.md — Current Task: App Detail/Info Pages + +> Read CLAUDE.md first for project context, workspace layout, and build instructions. +> This file describes the current task to implement. + +## Overview + +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. + +The first app to get this treatment is **RoMM** (retro game ROM manager), which has optional +metadata provider API keys (IGDB, SteamGridDB, ScreenScraper, MobyGames). + +## Architecture context + +- 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()` + +## 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" .}} + + + + +
+ +
+ {{if .AppInfo.Tagline}} +

{{.AppInfo.Tagline}}

+ {{else}} +

{{.Meta.Description}}

+ {{end}} +
+ ~{{.Meta.Resources.MemRequest}} RAM + {{.Meta.Category}} + {{if .Meta.Resources.NeedsHDD}}HDD szükséges{{end}} + {{if .Meta.Resources.PiCompatible}}Pi kompatibilis{{else}}Csak x86{{end}} +
+
+
+ + +
+ + + +
+ +{{if .HasAppInfo}} +
+ {{if .AppInfo.UseCases}} +
+

🎯 Mire használható?

+
    + {{range .AppInfo.UseCases}}
  • {{.}}
  • {{end}} +
+
+ {{end}} + + {{if .AppInfo.FirstSteps}} +
+

🚀 Első lépések

+
    + {{range .AppInfo.FirstSteps}}
  1. {{.}}
  2. {{end}} +
+
+ {{end}} + + {{if .AppInfo.Prerequisites}} +
+

📋 Előfeltételek

+
    + {{range .AppInfo.Prerequisites}}
  • {{.}}
  • {{end}} +
+
+ {{end}} + + {{if .AppInfo.DefaultCreds}} +
+

🔑 Alapértelmezett belépés

+

{{.AppInfo.DefaultCreds}}

+

⚠️ Az első bejelentkezés után azonnal változtasd meg!

+
+ {{end}} + + {{if .AppInfo.DocsURL}} +
+

📖 Dokumentáció

+

Hivatalos dokumentáció ↗

+
+ {{end}} +
+{{end}} + +{{if .HasOptionalConfig}} +
+

⚙️ Opcionális beállítások

+ {{range .OptionalConfig}} +
+

{{.Group}}

+ {{if .Description}}

{{.Description}}

{{end}} + +
+ {{range .Fields}} +
+ + {{if .HelpText}}

{{.HelpText}}

{{end}} + {{if .HelpURL}}

Regisztrációs útmutató ↗

{{end}} + +
+ {{end}} +
+
+ {{end}} + +
+ + +
+
+ + +{{end}} + +{{template "layout_end" .}} +{{end}} +` +``` + +#### 5c. Add CSS to `cssContent` + +Append these styles at the end of `cssContent` (before the closing backtick): + +```css +/* --- App Info Page --- */ +.app-info-hero { + display: flex; + align-items: center; + gap: 1.5rem; + padding: 1.5rem; + background: var(--bg-card); + border-radius: var(--radius); + border: 1px solid var(--border-color); + margin-bottom: 1.5rem; +} +.app-info-logo { + width: 80px; + height: 80px; + border-radius: 12px; + object-fit: contain; + background: var(--bg-secondary); + padding: 10px; + flex-shrink: 0; +} +.app-info-tagline { + font-size: 1.1rem; + color: var(--text-primary); + margin: 0 0 .75rem 0; +} +.app-screenshots { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; + overflow-x: auto; + padding-bottom: .5rem; +} +.app-screenshot { + max-height: 220px; + border-radius: var(--radius); + border: 1px solid var(--border-color); + object-fit: cover; +} +.app-info-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} +.app-info-card { + background: var(--bg-card); + border-radius: var(--radius); + padding: 1.25rem; + border: 1px solid var(--border-color); +} +.app-info-card h3 { + margin: 0 0 .75rem 0; + font-size: .95rem; + color: var(--text-primary); +} +.app-info-list { + margin: 0; + padding-left: 1.25rem; + color: var(--text-secondary); + font-size: .9rem; + line-height: 1.6; +} +.app-info-creds { + font-family: monospace; + font-size: 1rem; + color: var(--accent-light); + background: var(--bg-secondary); + padding: .5rem .75rem; + border-radius: 4px; + display: inline-block; + margin: 0 0 .5rem 0; +} +.app-info-creds-warn { + color: var(--orange); + font-size: .85rem; + margin: 0; +} +.app-info-link { + color: var(--accent-light); + text-decoration: none; +} +.app-info-link:hover { text-decoration: underline; } + +/* Optional config section */ +.app-optional-config { + background: var(--bg-card); + border-radius: var(--radius); + padding: 1.5rem; + border: 1px solid var(--border-color); + margin-bottom: 1.5rem; +} +.app-optional-config h3 { + margin: 0 0 1rem 0; + font-size: 1.05rem; +} +.config-group { + margin-bottom: 1.5rem; +} +.config-group h4 { + margin: 0 0 .5rem 0; + font-size: .95rem; + color: var(--text-primary); +} +.config-group-desc { + color: var(--text-secondary); + font-size: .85rem; + margin: 0 0 1rem 0; +} +.config-fields { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 1rem; +} +.config-field { + display: flex; + flex-direction: column; + gap: .25rem; +} +.config-field label { + font-size: .85rem; + font-weight: 500; + color: var(--text-primary); +} +.config-field-help { + font-size: .8rem; + color: var(--text-secondary); + margin: 0; + line-height: 1.4; +} +.config-field-help a { + color: var(--accent-light); + text-decoration: none; +} +.config-field-help a:hover { text-decoration: underline; } +.config-input { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 4px; + padding: .5rem .75rem; + color: var(--text-primary); + font-size: .9rem; + font-family: monospace; + width: 100%; + box-sizing: border-box; +} +.config-input:focus { + outline: none; + border-color: var(--accent-blue); +} +.config-actions { + display: flex; + align-items: center; + gap: 1rem; + margin-top: 1rem; +} +.config-save-status { + font-size: .9rem; + transition: opacity .3s; +} +.config-save-ok { color: var(--green); } +.config-save-err { color: var(--red); } +.meta-badge-warn { + background: rgba(255, 152, 0, 0.1) !important; + color: var(--orange) !important; +} +.meta-badge-ok { + background: rgba(76, 175, 80, 0.1) !important; + color: var(--green) !important; +} +``` + +--- + +### 6. Update navigation links in `templates.go` + +#### 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: + +```yaml +# ============================================================================= +# .felhom.yml — App metadata for felhom-controller +# ============================================================================= + +# --- Display info (shown on dashboard) --- +display_name: "RomM" +description: "Retró játékgyűjtemény kezelő" +category: "media" +subdomain: "arcade" +slug: "romm" + +# --- Resource hints (displayed on deploy screen) --- +resources: + mem_request: "300M" + mem_limit: "1024M" + pi_compatible: false + needs_hdd: true + +# --- Deploy fields (first deployment only) --- +deploy_fields: + - env_var: DOMAIN + label: "Domain" + type: domain + description: "A szerver domain neve" + locked_after_deploy: true + + - env_var: DB_PASSWORD + label: "Adatbázis jelszó" + type: secret + generate: "password:24" + locked_after_deploy: true + + - env_var: MYSQL_ROOT_PASSWORD + label: "MariaDB root jelszó" + type: secret + generate: "password:24" + locked_after_deploy: true + + - env_var: ROMM_AUTH_SECRET_KEY + label: "Hitelesítési titkosítási kulcs" + type: secret + generate: "hex:32" + locked_after_deploy: true + + - env_var: HDD_PATH + label: "Adattárolási útvonal" + type: path + required: true + placeholder: "/mnt/hdd_1" + description: "A külső merevlemez elérési útja, ahol a ROM-ok és borítóképek tárolódnak" + locked_after_deploy: true + +# --- App info (info page content) --- +app_info: + tagline: "Retró játékgyűjtemény kezelő, böngésző és lejátszó" + default_creds: "admin / admin" + docs_url: "https://github.com/rommapp/romm/wiki" + + use_cases: + - "Retró játékgyűjtemény rendszerezése és böngészése webes felületen" + - "Játékok metaadatainak automatikus letöltése — borítók, leírások, értékelések" + - "Platformonkénti szűrés és keresés a teljes gyűjteményben" + - "ROM fájlok webes feltöltése és letöltése" + - "Többfelhasználós hozzáférés a háztartás tagjai számára" + + first_steps: + - "Nyisd meg az arcade.DOMAIN címet a böngészőben" + - "Jelentkezz be az alapértelmezett admin / admin fiókkal" + - "Változtasd meg azonnal a jelszót a Settings menüben" + - "Töltsd fel a ROM fájlokat a library mappába (platform/játéknév struktúrával)" + - "Indíts egy Scan-t a bal oldali menüben a ROM-ok beolvasásához" + - "Opcionális: állíts be metaadat-szolgáltatókat a borítóképek és leírások automatikus letöltéséhez (lásd lent)" + + prerequisites: + - "Külső HDD szükséges a ROM fájlok és borítóképek tárolásához" + - "Legalább 1 GB szabad RAM ajánlott (MariaDB + Redis + RomM)" + - "ROM fájlok platform mappákba rendezve (pl. library/gba/, library/snes/)" + +# --- Optional config (configurable before or after deployment) --- +optional_config: + - group: "Metaadat-szolgáltatók" + description: "Játékok borítóinak, leírásainak és értékeléseinek automatikus letöltéséhez. Legalább az IGDB beállítása ajánlott. Mindegyik ingyenesen használható regisztráció után." + fields: + - env_var: IGDB_CLIENT_ID + label: "IGDB Client ID" + type: text + help_url: "https://api-docs.igdb.com/#getting-started" + help_text: "1) Regisztrálj / jelentkezz be a Twitch fejlesztői portálon (dev.twitch.tv). 2) Hozz létre egy új alkalmazást (bármilyen névvel). 3) Másold be a Client ID-t." + + - env_var: IGDB_CLIENT_SECRET + label: "IGDB Client Secret" + type: secret_input + help_text: "A Twitch alkalmazásod Client Secret-je (a „New Secret" gombbal generálhatod)." + + - env_var: STEAMGRIDDB_API_KEY + label: "SteamGridDB API Key" + type: text + help_url: "https://www.steamgriddb.com/profile/preferences/api" + help_text: "Regisztrálj a SteamGridDB oldalon, majd a Preferences → API fül alatt kattints a „Generate API key" gombra." + + - env_var: SCREENSCRAPER_USER + label: "ScreenScraper felhasználónév" + type: text + help_url: "https://www.screenscraper.fr/" + help_text: "Regisztrálj a screenscraper.fr oldalon. A felhasználóneved lesz az API felhasználónév." + + - env_var: SCREENSCRAPER_PASSWORD + label: "ScreenScraper jelszó" + type: secret_input + help_text: "A screenscraper.fr fiókod jelszava." + + - env_var: MOBYGAMES_API_KEY + label: "MobyGames API Key" + type: text + help_url: "https://www.mobygames.com/info/api/" + help_text: "Regisztrálj a MobyGames oldalon, majd az API oldalon igényelj kulcsot. Részletes játékinformációkat és krediteket biztosít." +``` + +--- + +### 8. RoMM docker-compose.yml template — Add missing env vars + +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: + +```yaml + - SCREENSCRAPER_USER=${SCREENSCRAPER_USER:-} + - SCREENSCRAPER_PASSWORD=${SCREENSCRAPER_PASSWORD:-} + - MOBYGAMES_API_KEY=${MOBYGAMES_API_KEY:-} +``` + +--- + +## 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