Files
deploy-felhom-compose/TASK.md
T

382 lines
13 KiB
Markdown

# TASK.md — v0.4.1: App Filtering + Bugfixes
> Version bump: **v0.4.1**
> Scope: UI feature + configuration fixes
---
## Overview
Three items:
1. **Feature**: App filtering on the Alkalmazások (Stacks) page with clickable stat cards on the Vezérlőpult (Dashboard) page
2. **Bugfix**: felhom.demo-felhom.eu returns 404 — docker-compose.yml on demo node needs syncing
3. **Config**: Enable backup on demo node so the backup UI section appears
---
## Task 1: App Filtering
### Concept
**Alkalmazások page** gets filter tabs at the top:
```
[ Mind (51) ] [ Futó (4) ] [ Leállítva (0) ] [ Telepíthető (47) ]
```
**Vezérlőpult page** stat cards become clickable — clicking navigates to the Alkalmazások page with the corresponding filter pre-selected:
- Click "4 Futó alkalmazás" → `/stacks?filter=running`
- Click "0 Leállítva" → `/stacks?filter=stopped`
- Click "51 Összes alkalmazás" → `/stacks` (no filter = show all)
### Implementation: 100% client-side
No server/handler changes needed. All filtering is done via JavaScript show/hide on existing DOM elements.
### Changes to `stacks.html`
Add filter bar between the page header and the stack grid:
```html
<div class="filter-bar" id="filter-bar">
<button class="filter-btn active" data-filter="all">Mind <span class="filter-count" id="count-all"></span></button>
<button class="filter-btn" data-filter="running">Futó <span class="filter-count" id="count-running"></span></button>
<button class="filter-btn" data-filter="stopped">Leállítva <span class="filter-count" id="count-stopped"></span></button>
<button class="filter-btn" data-filter="available">Telepíthető <span class="filter-count" id="count-available"></span></button>
</div>
```
Add `data-filter-state` attribute to each stack card in the `{{range .Stacks}}` loop:
```html
<div class="stack-detail-card stack-state-{{stateColor .State}}"
data-filter-state="{{filterCategory .State .Deployed}}"
...>
```
Where `filterCategory` is a new template function that maps states to filter categories:
- `running` → states: running, starting, unhealthy, restarting (has containers, is "up")
- `stopped` → states: stopped, exited (deployed but not running)
- `available` → state: not_deployed (never deployed)
### New template function: `filterCategory`
Add to `funcmap.go`:
```go
"filterCategory": func(state stacks.ContainerState, deployed bool) string {
switch state {
case stacks.StateRunning, stacks.StateStarting, stacks.StateUnhealthy, stacks.StateRestarting:
return "running"
case stacks.StateStopped, stacks.StateExited, stacks.StatePaused:
return "stopped"
default:
if deployed {
return "stopped" // deployed but unknown state → treat as stopped
}
return "available"
}
},
```
### JavaScript for stacks.html
Add a `<script>` block at the bottom of `stacks.html` (before `{{template "layout_end" .}}`):
```javascript
(function() {
// Count cards per category
var cards = document.querySelectorAll('.stack-detail-card[data-filter-state]');
var counts = {all: cards.length, running: 0, stopped: 0, available: 0};
cards.forEach(function(card) {
var cat = card.getAttribute('data-filter-state');
if (counts[cat] !== undefined) counts[cat]++;
});
// Update count badges
['all', 'running', 'stopped', 'available'].forEach(function(f) {
var el = document.getElementById('count-' + f);
if (el) el.textContent = '(' + counts[f] + ')';
});
// Filter logic
function applyFilter(filter) {
cards.forEach(function(card) {
if (filter === 'all' || card.getAttribute('data-filter-state') === filter) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
// Update active button
document.querySelectorAll('.filter-btn').forEach(function(btn) {
btn.classList.toggle('active', btn.getAttribute('data-filter') === filter);
});
// Update URL without reload
var url = new URL(window.location);
if (filter === 'all') {
url.searchParams.delete('filter');
} else {
url.searchParams.set('filter', filter);
}
history.replaceState(null, '', url);
}
// Button click handlers
document.querySelectorAll('.filter-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
applyFilter(this.getAttribute('data-filter'));
});
});
// Apply filter from URL on page load
var urlFilter = new URLSearchParams(window.location.search).get('filter');
if (urlFilter && ['running', 'stopped', 'available'].indexOf(urlFilter) !== -1) {
applyFilter(urlFilter);
}
})();
```
Key behaviors:
- Reads `?filter=running` from URL on page load → applies filter immediately
- Clicking a filter button updates URL via `history.replaceState` (no page reload)
- Counts are computed from the DOM on page load (no server-side count needed)
- "Mind" button removes the filter param from URL
- Hidden cards use `display: none` (simple, works with any layout)
### Changes to `dashboard.html`
Make the three stat cards clickable. Wrap each in an `<a>` tag or add `onclick`:
```html
<div class="stats-grid">
<a href="/stacks?filter=running" class="stat-card stat-running">
<div class="stat-value">{{.RunningCount}}</div>
<div class="stat-label">Futó alkalmazás</div>
</a>
<a href="/stacks?filter=stopped" class="stat-card stat-stopped">
<div class="stat-value">{{.StoppedCount}}</div>
<div class="stat-label">Leállítva</div>
</a>
<a href="/stacks" class="stat-card stat-total">
<div class="stat-value">{{.TotalCount}}</div>
<div class="stat-label">Összes alkalmazás</div>
</a>
</div>
```
Change `<div>` to `<a>` for the stat cards. This requires a small CSS fix to ensure `<a>` tags inherit the card styling (remove text-decoration, inherit color).
### CSS changes (`style.css`)
Add filter bar styles:
```css
/* --- Filter bar --- */
.filter-bar {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.filter-btn {
padding: 0.4rem 1rem;
border-radius: 20px;
border: 1px solid var(--border-color);
background: var(--bg-secondary);
color: var(--text-secondary);
cursor: pointer;
font-size: 0.85rem;
font-family: inherit;
transition: all 0.2s;
}
.filter-btn:hover {
border-color: var(--accent-blue);
color: var(--text-primary);
}
.filter-btn.active {
background: var(--accent-blue);
border-color: var(--accent-blue);
color: #fff;
}
.filter-count {
opacity: 0.7;
font-size: 0.8rem;
}
```
Add clickable stat card styles (for the `<a>` change):
```css
a.stat-card {
text-decoration: none;
color: inherit;
transition: transform 0.15s, box-shadow 0.15s;
}
a.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
```
### Edge cases
- If all cards in a filter category are 0 (e.g., "Leállítva (0)") → button still shows, just with (0) count. Clicking it shows empty grid (the existing "Nincs elérhető alkalmazás" empty state won't show since cards exist but are hidden — this is OK, the (0) count is self-explanatory).
- Protected stacks (traefik, cloudflared, felhom-controller): Include them in the filter. They have `data-filter-state="running"` like any running stack.
- Orphaned stacks: Same — categorized by their actual state.
- Page refresh preserves filter via URL param.
- Back/forward navigation: `history.replaceState` means no extra history entries.
---
## Task 2: Fix felhom.demo-felhom.eu 404
### Confirmed root cause
The docker-compose.yml on the demo node (192.168.0.162) is **stale** — still from pre-v0.3.0:
**On demo node** (confirmed from troubleshooting.txt):
```yaml
- "traefik.http.routers.controller.rule=Host(`dashboard.${DOMAIN}`)" # ← WRONG
```
Also missing:
- `/sys:/host/sys:ro` volume mount (needed for temperature reading, added in v0.4.0)
- `felhom.managed=true` and `felhom.component=controller` labels
- Updated restic-password handling
**In repo** (correct):
```yaml
- "traefik.http.routers.controller.rule=Host(`felhom.${DOMAIN}`)" # ← CORRECT
```
The deploy script only does `sed -i` on the image tag, so docker-compose.yml changes from v0.3.0 and v0.4.0 were never applied to the demo node.
**DNS is fine** — Cloudflare has a wildcard CNAME (`*` → tunnel UUID), so `felhom.demo-felhom.eu` resolves through the tunnel. The 404 comes from Traefik not finding a matching `Host()` rule.
### Fix
**Step 1** — Sync docker-compose.yml from repo to demo node:
```bash
scp controller/docker-compose.yml kisfenyo@192.168.0.162:/opt/docker/felhom-controller/docker-compose.yml
```
**Step 2** — Recreate the controller with updated compose:
```bash
ssh kisfenyo@192.168.0.162 "cd /opt/docker/felhom-controller && sudo docker compose up -d"
```
This applies the new Traefik label (`felhom.${DOMAIN}`) and the `/sys` mount.
**Step 3** — Verify Cloudflare Tunnel public hostname config includes `felhom.demo-felhom.eu`. Since DNS uses a wildcard CNAME, the tunnel should already route it — but check the tunnel's public hostname entries in the Cloudflare Zero Trust dashboard. If only `dashboard.demo-felhom.eu` is listed, update it to `felhom.demo-felhom.eu`.
**Step 4** — Pi-hole DNS (if applicable): Update local DNS from `dashboard.demo-felhom.eu``felhom.demo-felhom.eu` pointing to `192.168.0.162`.
**Long-term**: The deploy process should sync the full docker-compose.yml, not just the image tag. This is a known gap — consider a controller self-update mechanism in a future version.
---
## Task 3: Enable backup on demo node
### Root cause
The backup UI section (`{{if .BackupEnabled}}`) exists in `dashboard.html` but `backup.enabled` is `false` (or unset) in the demo node's `controller.yaml`.
### Fix
Edit `/opt/docker/felhom-controller/controller.yaml` on demo-felhom (192.168.0.162):
```yaml
backup:
enabled: true
restic_repo: "/srv/backups/restic-repo"
restic_password_file: "/opt/docker/felhom-controller/data/restic-password"
db_dump_schedule: "02:30"
restic_schedule: "03:00"
retention:
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
prune_schedule: "sunday"
```
Ensure `/srv/backups` directory exists on the host:
```bash
sudo mkdir -p /srv/backups/db-dumps /srv/backups/restic-repo
```
Restart the controller: `docker compose restart felhom-controller`
The restic password will be auto-generated on first backup run. The "Biztonsági mentés" section should appear on the dashboard with "Még nem futott" status until the first scheduled or manual backup.
**Optional**: Create Healthchecks checks in status.felhom.eu and add the UUIDs to the monitoring config for full ping integration.
---
## Implementation order
### Step 1: Template function
1. Add `filterCategory` to `funcmap.go`
### Step 2: Stacks page filtering
1. Add `data-filter-state` attribute to stack cards in `stacks.html`
2. Add filter bar HTML
3. Add filter JavaScript
4. Add filter bar CSS to `style.css`
### Step 3: Dashboard clickable cards
1. Change stat card `<div>` to `<a>` with href in `dashboard.html`
2. Add `a.stat-card` CSS styles
### Step 4: Build, deploy, verify
1. Build v0.4.1
2. Deploy to demo node (this time, sync the FULL docker-compose.yml, not just image tag)
3. Verify filters work on Alkalmazások page
4. Verify stat card clicks navigate correctly
5. Verify `?filter=running` URL param works
### Step 5: Manual config fixes (for Viktor, NOT in code)
1. Fix Cloudflare Tunnel hostname
2. Enable backup in controller.yaml
3. Create /srv/backups directories
---
## Files to modify
```
internal/web/funcmap.go — add filterCategory function
internal/web/templates/stacks.html — filter bar + data attributes + JS
internal/web/templates/dashboard.html — stat cards as <a> links
internal/web/templates/style.css — filter bar + clickable card styles
```
## Files unchanged
No server-side handler changes. No config changes. No new files.
---
## Verification checklist
- [ ] Alkalmazások page shows filter bar with correct counts
- [ ] Clicking "Futó" shows only running apps
- [ ] Clicking "Leállítva" shows only stopped apps
- [ ] Clicking "Telepíthető" shows only undeployed apps
- [ ] Clicking "Mind" shows everything
- [ ] URL updates to `?filter=running` when filter active
- [ ] Page load with `?filter=running` in URL pre-applies filter
- [ ] Dashboard "Futó alkalmazás" card links to `/stacks?filter=running`
- [ ] Dashboard "Leállítva" card links to `/stacks?filter=stopped`
- [ ] Dashboard "Összes" card links to `/stacks`
- [ ] Stat cards show hover effect (slight lift + shadow)
- [ ] Protected stacks appear in correct filter category
- [ ] Filter works correctly after template sync (new apps appear in right category)