diff --git a/TASK.md b/TASK.md index 8597860..bd0fb9b 100644 --- a/TASK.md +++ b/TASK.md @@ -1,486 +1,257 @@ -# TASK: Infrastructure FileBrowser + Orphan Stack Handling + Catalog Fixes +# TASK.md — Controller Refactoring: Template Split, Server Decomposition, Domain Rename -**Priority order:** Task 1 → Task 2 → Task 3 (Task 3 is independent, can be done anytime) - -**Repositories involved:** -- `deploy-felhom-compose` — controller Go code, docker-setup.sh, hdd-setup.sh -- `app-catalog-felhom.eu` — template catalog +> **Goal:** Improve code organization for maintainability and Claude Code efficiency. +> No new features — purely structural refactoring + one config rename. +> +> **Version bump:** v0.3.0 (structural refactor milestone) --- -## Task 1: FileBrowser Quantum as Infrastructure Service +## Task 1: Split templates.go → go:embed (HIGH PRIORITY) -### Context +The `templates.go` file contains ALL HTML templates and CSS as Go string constants. +The file itself says: *"As the UI grows, switch to go:embed for easier editing."* +With 7 templates + full CSS, it's time. -FileBrowser Quantum (`gtstef/filebrowser:latest`) becomes a **mandatory infrastructure service** -deployed alongside Traefik, Cloudflared, and felhom-controller. It provides the customer with -permanent web-based access to their HDD data — this is critical for the orphan deletion workflow -(Task 2), where users need to retrieve data from deleted apps. +### 1.1 — Create template files directory -FileBrowser is **removed from the app catalog** and instead deployed by `docker-setup.sh` during -initial server setup, just like Traefik. +Create `controller/internal/web/templates/` with individual files: -### 1.1 — HDD Folder Structure Update (hdd-setup.sh) - -Add new user-facing folders to the HDD folder structure arrays in `hdd-setup.sh`: - -**Current structure:** ``` -${HDD_PATH}/ -├── media/ -│ ├── downloads/complete/ -│ ├── downloads/incomplete/ -│ ├── movies/ -│ ├── series/ -│ ├── music/ -│ └── books/ -├── storage/ -│ ├── immich/ -│ ├── nextcloud/ -│ ├── filebrowser/ ← REMOVE (filebrowser is now infra, doesn't need storage/) -│ ├── backups/local/ -│ └── backups/appdata/ -└── appdata/ +internal/web/templates/ +├── layout.html ← from layoutTmpl const +├── dashboard.html ← from dashboardTmpl const +├── stacks.html ← from stacksTmpl const (the stacks list page, NOT the old dashboard list) +├── login.html ← from loginTmpl const +├── logs.html ← from logsTmpl const +├── deploy.html ← from deployTmpl const +├── app_info.html ← from appInfoTmpl const +└── style.css ← from cssTemplate const ``` -**Updated structure:** -``` -${HDD_PATH}/ -├── Dokumentumok/ ← NEW: user documents (OnlyOffice, general files) -├── media/ -│ ├── downloads/complete/ -│ ├── downloads/incomplete/ -│ ├── movies/ -│ ├── series/ -│ ├── music/ -│ └── books/ -├── storage/ -│ ├── immich/ -│ ├── nextcloud/ -│ ├── backups/local/ -│ └── backups/appdata/ -└── appdata/ +Each `.html` file should contain ONLY the template content (the `{{define "name"}}...{{end}}` block). +Keep the existing template names (`layout_start`, `layout_end`, `dashboard`, `stacks`, `login`, etc.). + +### 1.2 — Create embed.go + +Create `controller/internal/web/embed.go`: + +```go +package web + +import "embed" + +//go:embed templates/*.html templates/*.css +var templateFS embed.FS ``` -Changes to `hdd-setup.sh`: -- Remove `"storage/filebrowser"` from `STORAGE_DIRS` array -- Add `"Dokumentumok"` as a new top-level entry (add a new `USER_DIRS` array, or add to existing) -- Ownership: same 1000:1000 as other dirs +### 1.3 — Update template loading in server.go (or the new funcmap.go, see Task 2) -### 1.2 — FileBrowser Docker Compose (Infrastructure) +Replace the current `loadTemplates()` method that parses the `allTemplates` const: -Create `/opt/docker/stacks/filebrowser/docker-compose.yml` during `docker-setup.sh` execution. +```go +func (s *Server) loadTemplates() { + funcMap := template.FuncMap{ /* ... existing funcs ... */ } -**Mount strategy — three tiers with different permissions:** + s.tmpl = template.Must( + template.New("").Funcs(funcMap).ParseFS(templateFS, "templates/*.html"), + ) +} +``` -| HDD Path | Container Mount | Access | Rationale | -|---|---|---|---| -| `${HDD_PATH}/storage/` | `/srv/storage` | **read-only** | App data, prevent accidental deletion | -| `${HDD_PATH}/media/` | `/srv/media` | **read-write** | User adds movies, music, books | -| `${HDD_PATH}/Dokumentumok/` | `/srv/Dokumentumok` | **read-write** | User documents (docx, xlsx, etc.) | +CSS serving: Instead of the `cssTemplate` const, read from `templateFS`: -**Docker Compose template:** +```go +func (s *Server) serveCSSHandler(w http.ResponseWriter, r *http.Request) { + data, err := templateFS.ReadFile("templates/style.css") + if err != nil { + http.Error(w, "CSS not found", 500) + return + } + w.Header().Set("Content-Type", "text/css; charset=utf-8") + w.Header().Set("Cache-Control", "public, max-age=3600") + w.Write(data) +} +``` + +Register this handler for `/static/style.css` in `ServeHTTP` (replace the current inline CSS serving). + +### 1.4 — Delete old string constants + +Remove from `templates.go`: +- `const allTemplates = ...` +- `const layoutTmpl = ...` +- `const dashboardTmpl = ...` +- `const stacksTmpl = ...` +- `const loginTmpl = ...` +- `const logsTmpl = ...` +- `const deployTmpl = ...` +- `const appInfoTmpl = ...` +- `const cssTemplate = ...` + +After this, `templates.go` should either be empty (delete it) or contain only the +felhom logo SVG constant if that's still embedded as a string (keep that one — it's small). + +### 1.5 — Verify the build + +- `go build ./cmd/controller/` must succeed +- `go:embed` requires Go 1.16+ (we're on 1.22, fine) +- Templates are still compiled into the binary — zero runtime file dependencies (same as before) +- Verify that the HTML files actually include the `{{define "name"}}...{{end}}` wrappers + (ParseFS needs them to register template names) + +### Important notes + +- The `` in layout.html already exists, + so CSS loading via the `/static/style.css` route should already work — just make sure + the handler reads from embed.FS instead of serving the const. +- The felhom logo SVG can stay as a Go const (it's small) or move to `templates/felhom-logo.svg` + and be served from embed.FS too. Either approach is fine. + +--- + +## Task 2: Split server.go into focused files (MEDIUM PRIORITY) + +Currently `server.go` handles: Server struct, auth/sessions, page handlers, template FuncMap, +asset serving, and HTTP routing. Split into: + +### 2.1 — Create `auth.go` + +Move from `server.go` to `internal/web/auth.go`: +- `type session struct` +- `const sessionCookieName`, `const sessionMaxAge` +- `RequireAuth()` middleware method +- `loginHandler()`, `loginPostHandler()`, `logoutHandler()` +- `createSession()`, `isValidSession()`, `cleanupSessions()` +- `renderLogin()` helper + +### 2.2 — Create `handlers.go` + +Move from `server.go` to `internal/web/handlers.go`: +- `baseData()` helper +- `dashboardHandler()` +- `stacksHandler()` +- `deployPageHandler()` +- `deployPagePostHandler()` (if it exists as separate handler) +- `appDetailHandler()` +- `logsPageHandler()` + +### 2.3 — Create `funcmap.go` + +Move from `server.go` to `internal/web/funcmap.go`: +- The entire `template.FuncMap` definition from `loadTemplates()` +- Extract it as a standalone function: `func (s *Server) templateFuncMap() template.FuncMap` +- Then `loadTemplates()` becomes a clean 3-liner calling `templateFuncMap()` + `ParseFS` + +### 2.4 — Keep in server.go + +After the split, `server.go` should contain only: +- `type Server struct` +- `func NewServer()` +- `func (s *Server) loadTemplates()` (now a 3-liner) +- `func (s *Server) ServeHTTP()` (HTTP routing dispatch) +- `func (s *Server) render()` helper +- Static file/asset serving handlers (`serveStaticFile`, `serveCSSHandler`, `serveLogoHandler`) + +### 2.5 — Verify the split + +All files are in `package web` — no import changes needed within the package. +The `Server` struct and all its methods are accessible across files in the same package. + +Run `go build ./cmd/controller/` to verify everything compiles. + +--- + +## Task 3: Rename controller domain from `dashboard.*` to `felhom.*` (LOW PRIORITY) + +### 3.1 — Update controller's docker-compose.yml + +In `controller/docker-compose.yml`, change the Traefik label: ```yaml -# FileBrowser Quantum — Infrastructure file manager -# Domain: files.${DOMAIN} -# Deployed by docker-setup.sh — do NOT remove -# -# Mount permissions: -# /srv/storage/ → HDD storage/ (READ-ONLY — app data) -# /srv/media/ → HDD media/ (read-write — user media) -# /srv/Dokumentumok/ → HDD Dokumentumok/ (read-write — user documents) - -services: - filebrowser: - image: gtstef/filebrowser:latest - container_name: filebrowser - restart: unless-stopped - environment: - - TZ=Europe/Budapest - volumes: - - filebrowser_data:/home/filebrowser/data - - ${HDD_PATH}/storage:/srv/storage:ro - - ${HDD_PATH}/media:/srv/media - - ${HDD_PATH}/Dokumentumok:/srv/Dokumentumok - networks: - - traefik-public - deploy: - resources: - limits: - memory: 256M - healthcheck: - test: ["CMD", "wget", "--spider", "-q", "http://localhost:80/health"] - interval: 30s - timeout: 5s - retries: 3 - start_period: 15s - labels: - - "traefik.enable=true" - - "traefik.http.routers.filebrowser.rule=Host(`files.${DOMAIN}`)" - - "traefik.http.routers.filebrowser.entrypoints=websecure" - - "traefik.http.routers.filebrowser.tls=true" - - "traefik.http.routers.filebrowser.tls.certresolver=letsencrypt" - - "traefik.http.services.filebrowser.loadbalancer.server.port=80" - - "traefik.docker.network=traefik-public" - -volumes: - filebrowser_data: - -networks: - traefik-public: - external: true +# OLD: +- "traefik.http.routers.controller.rule=Host(`dashboard.${DOMAIN}`)" +# NEW: +- "traefik.http.routers.controller.rule=Host(`felhom.${DOMAIN}`)" ``` -**Default credentials:** admin / admin (user should change on first login). +### 3.2 — Update docker-setup.sh -**NOTE:** The healthcheck endpoint `/health` should be verified against FileBrowser Quantum docs. -If it doesn't exist, fall back to `wget --spider -q http://localhost:80/`. +In `controller/scripts/docker-setup.sh`, update `print_summary()` output: +- Any reference to `dashboard.${BASE_DOMAIN}` → `felhom.${BASE_DOMAIN}` -### 1.3 — Deploy in docker-setup.sh +Also check install_controller() if it generates compose files or prints URLs. -Add a new step in `docker-setup.sh` that deploys FileBrowser **after** Traefik is running -and **after** HDD is mounted (HDD_PATH must be known). +### 3.3 — Update controller.yaml.example -**Implementation notes:** -- The step should come after `install_traefik()` and after HDD detection/mount -- Requires `HDD_PATH` to be set — if no HDD is configured, **skip FileBrowser deployment** - and log a warning: "FileBrowser skipped — no HDD path configured. Deploy manually after HDD setup." -- Create the compose file from template (substitute `${DOMAIN}` and `${HDD_PATH}`) -- Create a `.env` file in the filebrowser stack dir with `DOMAIN` and `HDD_PATH` -- `docker compose up -d` -- Verify container is running +If there's any reference to `dashboard.*` in the example config or comments, update to `felhom.*`. -**New function:** `install_filebrowser()` +### 3.4 — Update documentation -### 1.4 — Add to Protected Stacks +In CLAUDE.md build/deploy workflow sections, update any `dashboard.` references to `felhom.`. -Update `controller.yaml.example` to include filebrowser in the protected list: +### 3.5 — Cloudflare Tunnel public hostname (MANUAL — not code) -```yaml -stacks: - protected: - - "traefik" - - "cloudflared" - - "felhom-controller" - - "filebrowser" # ← ADD -``` +**Reminder for Viktor:** After deploying, manually update the Cloudflare Tunnel +public hostname in the Zero Trust dashboard: +- Old: `dashboard.demo-felhom.eu` → Traefik +- New: `felhom.demo-felhom.eu` → Traefik -Also update any hardcoded protected stack references in documentation/README. +### 3.6 — Pi-hole DNS (MANUAL — not code) -### 1.5 — Remove FileBrowser from App Catalog - -In `app-catalog-felhom.eu` repository: -- **Delete** `templates/filebrowser/` directory (docker-compose.yml + .felhom.yml) -- **Delete** `existing-appinfo/filebrowser-appinfo.yml` if it exists -- FileBrowser should no longer appear in the "Alkalmazások" catalog on the dashboard - -### 1.6 — Dashboard UI for Infrastructure Services - -Currently the dashboard shows catalog apps. Infrastructure services (traefik, cloudflared, -controller, filebrowser) are hidden. Consider adding a small "Rendszer" (System) section -at the bottom of the sidebar or dashboard that shows infrastructure service status. - -**This is optional / future work** — not blocking. The controller already knows about -protected stacks via `IsProtectedStack()`. The UI just needs to render them differently -if this section is added. +**Reminder for Viktor:** If there's a pi-hole local DNS record for `dashboard.demo-felhom.eu`, +update it to `felhom.demo-felhom.eu` (or rely on the wildcard `*.demo-felhom.eu` record). --- -## Task 2: Orphan Stack Detection and Deletion +## Task 4: Update documentation -### Context +### 4.1 — README.md -When an app is removed from the catalog (e.g., Stirling-PDF replaced by BentoPDF), its stack -directory may still exist in `/opt/docker/stacks/` with containers still deployed. These -"orphaned" stacks need to be visible on the dashboard with a clear state and deletable by the user. +- Update directory structure to show `internal/web/templates/` directory +- Update "Tech stack" section: "Templates: go:embed HTML files" instead of "Go string constants" +- Mention the `felhom.*` subdomain for the controller +- Update file tree showing the new files (embed.go, auth.go, handlers.go, funcmap.go) -Because FileBrowser (Task 1) gives users permanent access to their HDD data, the delete flow -can safely remove Docker volumes while informing users their HDD files are still accessible. +### 4.2 — CLAUDE.md -### 2.1 — New Stack State: `orphaned` +- Update workspace layout to reflect the new `internal/web/` file structure +- Update "Tech stack" section +- Update any `dashboard.*` references to `felhom.*` +- Note the go:embed pattern for future template additions -Add a new constant in the stacks package: +### 4.3 — CONTEXT.md -```go -StateOrphaned ContainerState = "orphaned" -``` +- Add session entry documenting the refactoring +- Note: templates moved from Go string constants to go:embed HTML files +- Note: server.go split into auth.go, handlers.go, funcmap.go +- Note: controller domain changed from dashboard.* to felhom.* +- Update version to v0.3.0 -An orphaned stack is defined as: -- Has a `docker-compose.yml` in `/opt/docker/stacks//` -- Has `app.yaml` with `deployed: true` -- Does **NOT** have a matching template in the synced catalog +### 4.4 — BUILDING.md -### 2.2 — Orphan Detection in ScanStacks() - -After the existing scan loop in `ScanStacks()`, add orphan detection: - -```go -// After scanning all stack dirs, check which deployed stacks have no catalog template -catalogTemplates := m.getCatalogTemplateSlugs() // returns set of slugs from synced catalog - -for name, stack := range m.stacks { - if stack.Protected { - continue // infrastructure stacks are never orphaned - } - if !stack.Deployed { - continue // not deployed = just an available template, not orphaned - } - if !catalogTemplates[name] { - stack.Orphaned = true - } -} -``` - -**Add `Orphaned` field to Stack struct:** - -```go -type Stack struct { - Name string `json:"name"` - Meta Metadata `json:"meta"` - ComposePath string `json:"compose_path"` - State ContainerState `json:"state"` - Deployed bool `json:"deployed"` - Protected bool `json:"protected"` - Orphaned bool `json:"orphaned"` // ← ADD - Containers []ContainerInfo `json:"containers"` - AppConfig *AppConfig `json:"app_config,omitempty"` - LastUpdated time.Time `json:"last_updated"` -} -``` - -**`getCatalogTemplateSlugs()`** needs to read the synced catalog directory and return -a `map[string]bool` of all template slugs that have a `docker-compose.yml`. The synced -catalog lives at the path configured in `git.local_path` or wherever the catalog sync -stores templates after git pull. Check the existing `catalogsync` package for the exact path. - -### 2.3 — Dashboard UI for Orphaned Stacks - -Orphaned stacks appear in the deployed apps list with distinct visual treatment: - -**Visual styling:** -- Left border: amber/yellow (instead of green for running or gray for stopped) -- Badge: `Elavult` (Deprecated) — amber background, dark text -- App name still shown from `.felhom.yml` metadata (if available) or directory name -- Show current state (running/stopped) alongside the orphan badge - -**Available actions for orphaned stacks:** -- ✅ Start / Stop (normal controls — user may need to run it briefly) -- ✅ View logs -- ✅ **Törlés** (Delete) button — NEW, only shown for orphaned stacks -- ❌ No "Frissítés" (Update) — no catalog template to update from -- ❌ No "Beállítások" (Settings) — no deploy_fields to configure - -### 2.4 — Delete API Endpoint - -**Endpoint:** `DELETE /api/stacks/{name}` - -**Request body:** -```json -{ - "remove_hdd_data": false -} -``` - -**Preconditions (return 409 Conflict if violated):** -- Stack must be **stopped** (State != running). Force the user to stop first. -- Stack must be **orphaned** (for now — catalog apps cannot be deleted, only stopped). - In the future this could be relaxed, but for safety, start with orphan-only deletion. - -**Execution steps:** - -``` -1. Verify preconditions (stopped + orphaned) -2. Read docker-compose.yml to identify: - a. Named Docker volumes (from `volumes:` top-level section) - b. HDD bind mounts (paths starting with ${HDD_PATH}) -3. Run: docker compose down --rmi local --volumes - - This removes containers, local images, AND named Docker volumes (SSD data) - - Named volumes (configs, databases, caches) are always removed — they're useless - without the app and are the #1 cause of "Docker ate my disk space" -4. If remove_hdd_data == true: - a. For each HDD bind mount found in step 2: - - Calculate size: du -sh - - Remove: rm -rf - - Log: "[INFO] Removed HDD data: ()" - b. WARNING: Never rm -rf ${HDD_PATH} itself or ${HDD_PATH}/media/ or - ${HDD_PATH}/Dokumentumok/ — only remove app-specific subdirectories - like ${HDD_PATH}/storage/paperless/ or ${HDD_PATH}/storage/immich/ -5. Remove stack directory: rm -rf /opt/docker/stacks// -6. Log the complete delete action with timestamp -7. Trigger ScanStacks() to refresh dashboard -8. Return 200 OK with summary -``` - -**Response body:** -```json -{ - "deleted": "stirling-pdf", - "volumes_removed": ["stirling_pdf_data"], - "hdd_paths_removed": [], - "hdd_paths_preserved": ["/mnt/hdd_1/storage/stirling-pdf (245 MB)"] -} -``` - -**Safety guards:** -- Protected stacks can never be deleted (check `IsProtectedStack()`) -- Running stacks can never be deleted (must stop first) -- Only orphaned stacks can be deleted (for now) -- HDD data deletion is opt-in (default false) -- Never delete top-level HDD directories (media/, storage/, Dokumentumok/) -- Log every delete action with full details - -### 2.5 — HDD Data Discovery for Delete Dialog - -The delete confirmation dialog needs to show what HDD data exists and its size. - -**New endpoint:** `GET /api/stacks/{name}/hdd-data` - -Parses the stack's `docker-compose.yml` to find HDD bind mounts, checks if paths exist -on disk, and returns size info: - -```json -{ - "stack": "stirling-pdf", - "hdd_paths": [ - { - "path": "/mnt/hdd_1/storage/stirling-pdf", - "size_bytes": 256901120, - "size_human": "245 MB", - "exists": true - } - ], - "has_hdd_data": true -} -``` - -If no HDD bind mounts exist (SSD-only app like Vaultwarden, Mealie), return: -```json -{ - "stack": "vaultwarden", - "hdd_paths": [], - "has_hdd_data": false -} -``` - -### 2.6 — Delete Confirmation Dialog (UI) - -**Full dialog (when HDD data exists):** -``` -┌──────────────────────────────────────────────────────┐ -│ Stirling-PDF törlése │ -│ │ -│ ⚠ Ez az alkalmazás már nem érhető el a │ -│ katalógusban. │ -│ │ -│ Az alkalmazás eltávolítása magában foglalja a │ -│ konténereket, beállításokat és belső adatbázist. │ -│ │ -│ ☐ Felhasználói adatok törlése │ -│ 📁 /srv/storage/stirling-pdf (245 MB) │ -│ ℹ Ha nem törli, a Fájlkezelőben továbbra is │ -│ elérheti ezeket a fájlokat. │ -│ │ -│ [Mégse] [Törlés] │ -└──────────────────────────────────────────────────────┘ -``` - -Notes: -- Show the FileBrowser-relative path (`/srv/storage/...`) not the system path — this is - what the user sees in FileBrowser -- The "Felhasználói adatok törlése" checkbox is **unchecked by default** -- The info hint reminds users about FileBrowser access (Task 1 must be completed first) -- "Törlés" button should be red/destructive styling - -**Simple dialog (no HDD data — SSD-only apps):** -``` -┌──────────────────────────────────────────────────────┐ -│ Vaultwarden törlése │ -│ │ -│ ⚠ Ez az alkalmazás már nem érhető el a │ -│ katalógusban. │ -│ │ -│ Az alkalmazás és minden adata véglegesen törlődik. │ -│ │ -│ [Mégse] [Törlés] │ -└──────────────────────────────────────────────────────┘ -``` - -No checkbox needed — there's nothing optional to preserve. - -### 2.7 — Router Registration - -Add to the API router: - -```go -r.HandleFunc("/api/stacks/{name}/hdd-data", r.getStackHDDData).Methods("GET") -r.HandleFunc("/api/stacks/{name}", r.deleteStack).Methods("DELETE") -``` - -Both require authentication (same as existing stack endpoints). +- Update the structure check in build.sh verification section if needed + (the `internal/web/templates/` directory should exist now) --- -## Task 3: App Catalog Fixes +## Implementation order -These are independent template fixes in the `app-catalog-felhom.eu` repository. +1. **Task 1** (templates.go → go:embed) — do this first, biggest impact +2. **Task 2** (server.go split) — do this second, leverages the cleaner templates +3. **Task 3** (domain rename) — small, do last +4. **Task 4** (docs) — update after all code changes -### 3.1 — BentoPDF: Change Subdomain to pdf.* +## Verification checklist - - -### 3.2 — Calibre-Web → Calibre-Web-Automated - - - -### 3.3 — FileBrowser → FileBrowser Quantum (Catalog Removal) - -Since FileBrowser is now infrastructure (Task 1), **remove it from the catalog entirely:** - -- **Delete** `templates/filebrowser/` directory -- **Delete** `existing-appinfo/filebrowser-appinfo.yml` -- The existing `filebrowser` catalog entry in any customer's deployed stacks will become - orphaned (Task 2 handles this gracefully) - -**Note:** If a customer already has the old catalog-based FileBrowser deployed, it will show -as orphaned after catalog sync. They can delete it via the orphan workflow. The infrastructure -FileBrowser (Task 1) will already be running at `files.${DOMAIN}`. - ---- - -## Implementation Checklist - -### deploy-felhom-compose repository - -- [ ] **hdd-setup.sh**: Add `Dokumentumok/` to folder structure, remove `storage/filebrowser` -- [ ] **docker-setup.sh**: Add `install_filebrowser()` function -- [ ] **controller.yaml.example**: Add `filebrowser` to `stacks.protected` list -- [ ] **stacks/manager.go** (or equivalent): - - [ ] Add `Orphaned` field to `Stack` struct - - [ ] Add `StateOrphaned` constant - - [ ] Add orphan detection in `ScanStacks()` - - [ ] Add `getCatalogTemplateSlugs()` helper -- [ ] **stacks/delete.go** (new file or add to manager): - - [ ] `DeleteStack()` method with volume + HDD cleanup - - [ ] `GetStackHDDData()` method for size discovery - - [ ] HDD path parsing from docker-compose.yml - - [ ] Safety guards (protected, running, top-level dir protection) -- [ ] **api/router.go**: - - [ ] `DELETE /api/stacks/{name}` endpoint - - [ ] `GET /api/stacks/{name}/hdd-data` endpoint -- [ ] **templates/dashboard.html** (or relevant UI template): - - [ ] Orphan badge styling (amber) - - [ ] Delete button for orphaned stacks - - [ ] Delete confirmation dialog with HDD data info - - [ ] FileBrowser hint in delete dialog -- [ ] **README.md**: Update protected stacks list, document delete flow - -### app-catalog-felhom.eu repository - -- [ ] Delete `templates/stirling-pdf/` (if exists) -- [ ] Delete `templates/filebrowser/` (moved to infra) -- [ ] Delete `existing-appinfo/filebrowser-appinfo.yml` -- [ ] Update `templates/bentopdf/` — subdomain `bento.*` → `pdf.*` -- [ ] Replace `templates/calibre-web/` with calibre-web-automated version -- [ ] Verify all YAML files parse without errors -- [ ] **README.md**: Update accordingly +- [ ] `go build ./cmd/controller/` compiles successfully +- [ ] All 7 HTML templates render correctly (login, dashboard, stacks, deploy, app_info, logs, layout) +- [ ] CSS loads at `/static/style.css` +- [ ] Felhom logo SVG loads at `/static/felhom-logo.svg` +- [ ] App logos/screenshots still serve from `/assets/` +- [ ] Auth (login/logout/session) works unchanged +- [ ] Stack operations (start/stop/deploy) work unchanged +- [ ] Controller accessible at `felhom.demo-felhom.eu` (after CF tunnel update) +- [ ] No broken links or template errors in browser console +- [ ] Build + push via build.sh works +- [ ] Deploy on demo-felhom works \ No newline at end of file