# TASK: Infrastructure FileBrowser + Orphan Stack Handling + Catalog Fixes **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 --- ## Task 1: FileBrowser Quantum as Infrastructure Service ### Context 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. FileBrowser is **removed from the app catalog** and instead deployed by `docker-setup.sh` during initial server setup, just like Traefik. ### 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/ ``` **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/ ``` 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.2 — FileBrowser Docker Compose (Infrastructure) Create `/opt/docker/stacks/filebrowser/docker-compose.yml` during `docker-setup.sh` execution. **Mount strategy — three tiers with different permissions:** | 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.) | **Docker Compose template:** ```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 ``` **Default credentials:** admin / admin (user should change on first login). **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/`. ### 1.3 — Deploy in docker-setup.sh 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). **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 **New function:** `install_filebrowser()` ### 1.4 — Add to Protected Stacks Update `controller.yaml.example` to include filebrowser in the protected list: ```yaml stacks: protected: - "traefik" - "cloudflared" - "felhom-controller" - "filebrowser" # ← ADD ``` Also update any hardcoded protected stack references in documentation/README. ### 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. --- ## Task 2: Orphan Stack Detection and Deletion ### Context 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. 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. ### 2.1 — New Stack State: `orphaned` Add a new constant in the stacks package: ```go StateOrphaned ContainerState = "orphaned" ``` 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 ### 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). --- ## Task 3: App Catalog Fixes These are independent template fixes in the `app-catalog-felhom.eu` repository. ### 3.1 — BentoPDF: Change Subdomain to pdf.* ### 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