13 KiB
TASK.md — v0.4.1: App Filtering + Bugfixes
Version bump: v0.4.1 Scope: UI feature + configuration fixes
Overview
Three items:
- Feature: App filtering on the Alkalmazások (Stacks) page with clickable stat cards on the Vezérlőpult (Dashboard) page
- Bugfix: felhom.demo-felhom.eu returns 404 — docker-compose.yml on demo node needs syncing
- 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:
<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:
<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:
"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" .}}):
(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=runningfrom 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:
<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:
/* --- 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):
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.replaceStatemeans 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):
- "traefik.http.routers.controller.rule=Host(`dashboard.${DOMAIN}`)" # ← WRONG
Also missing:
/sys:/host/sys:rovolume mount (needed for temperature reading, added in v0.4.0)felhom.managed=trueandfelhom.component=controllerlabels- Updated restic-password handling
In repo (correct):
- "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:
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:
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):
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:
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
- Add
filterCategorytofuncmap.go
Step 2: Stacks page filtering
- Add
data-filter-stateattribute to stack cards instacks.html - Add filter bar HTML
- Add filter JavaScript
- Add filter bar CSS to
style.css
Step 3: Dashboard clickable cards
- Change stat card
<div>to<a>with href indashboard.html - Add
a.stat-cardCSS styles
Step 4: Build, deploy, verify
- Build v0.4.1
- Deploy to demo node (this time, sync the FULL docker-compose.yml, not just image tag)
- Verify filters work on Alkalmazások page
- Verify stat card clicks navigate correctly
- Verify
?filter=runningURL param works
Step 5: Manual config fixes (for Viktor, NOT in code)
- Fix Cloudflare Tunnel hostname
- Enable backup in controller.yaml
- 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=runningwhen filter active - Page load with
?filter=runningin 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)