Files
deploy-felhom-compose/TASK.md
T

13 KiB

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:

<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=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:

<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.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):

- "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):

- "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.eufelhom.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

  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)