382 lines
13 KiB
Markdown
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) |