updated TASK for refactoring

This commit is contained in:
2026-02-15 10:23:02 +01:00
parent 63d81088bd
commit b30c01ac9e
+208 -437
View File
@@ -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 `<link rel="stylesheet" href="/static/style.css">` 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/<name>/`
- 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 <path>
- Remove: rm -rf <path>
- Log: "[INFO] Removed HDD data: <path> (<size>)"
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/<name>/
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
<should be done already, verify>
### 3.2 — Calibre-Web → Calibre-Web-Automated
<should be done already, verify>
### 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