diff --git a/.Jules/palette.md b/.Jules/palette.md index b687358..67867bd 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -34,3 +34,8 @@ **Learning:** In single-page applications die met toetsenbord en schermlezer worden gebruikt, gaat de focus verloren als een overlay sluit zonder focusherstel. Leg `document.activeElement` vast bij het openen van een overlay en herstel de focus bij sluiten, annuleren of succesvol opslaan. **Action:** Leg altijd het actieve element vast dat de overlay opent en herstel de focus bij sluiten, annuleren of succesvolle voltooiing. + +## 2026-08-02 - [Profile Empty State Recovery Actions] + +**Learning:** For tabbed or filtered single-page dashboard interfaces where empty states can be caused by profile-level filtering, offering a contextual "Switch profile to core" recovery action (using the `.view-empty-link` class) prevented users from hitting dead ends and significantly improved usability and keyboard/screen-reader accessibility. +**Action:** Always provide direct, one-click recovery links to fallback to a valid profile state when custom profiles filter out all results in empty-state views. diff --git a/src/kater/web/dashboard.py b/src/kater/web/dashboard.py index 09f5d86..737e794 100644 --- a/src/kater/web/dashboard.py +++ b/src/kater/web/dashboard.py @@ -2246,7 +2246,25 @@ class ApiError extends Error { btn.onclick = resetRouteFilter; empty.appendChild(btn); } - } else { empty.textContent = 'No servers in this profile.'; } + if (activeProfile !== 'core') { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'view-empty-link'; + btn.textContent = 'Switch profile to core'; + btn.onclick = () => switchProfile('core'); + empty.appendChild(btn); + } + } else { + empty.textContent = 'No servers in this profile.'; + if (activeProfile !== 'core') { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'view-empty-link'; + btn.textContent = 'Switch profile to core'; + btn.onclick = () => switchProfile('core'); + empty.appendChild(btn); + } + } el.appendChild(empty); return; } @@ -3455,14 +3473,26 @@ class ApiError extends Error { empty.textContent = 'No servers match "' + catalogQuery + '" in this status.'; addLink('Clear search', clearCatalogSearch); addLink('Switch filter to all', resetCatalogFilter); + if (activeProfile !== 'core') { + addLink('Switch profile to core', () => switchProfile('core')); + } } else if (hasQuery) { empty.textContent = 'No servers match "' + catalogQuery + '".'; addLink('Clear search', clearCatalogSearch); + if (activeProfile !== 'core') { + addLink('Switch profile to core', () => switchProfile('core')); + } } else if (hasFilter) { empty.textContent = 'No servers in this status.'; addLink('Switch filter to all', resetCatalogFilter); + if (activeProfile !== 'core') { + addLink('Switch profile to core', () => switchProfile('core')); + } } else { - empty.textContent = 'No servers in this profile. Switch profiles in the top bar.'; + empty.textContent = 'No servers in this profile.'; + if (activeProfile !== 'core') { + addLink('Switch profile to core', () => switchProfile('core')); + } } grid.appendChild(empty); return; @@ -4203,6 +4233,14 @@ class ApiError extends Error { const empty = document.createElement('div'); empty.className = 'view-empty'; empty.textContent = 'No capabilities discoverable for the current profile.'; + if (activeProfile !== 'core') { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'view-empty-link'; + btn.textContent = 'Switch profile to core'; + btn.onclick = () => switchProfile('core'); + empty.appendChild(btn); + } capsEl.appendChild(empty); } else { for (const item of caps.slice(0, 50)) { diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 4e3ac61..8bd4b7b 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -85,6 +85,7 @@ def test_zero_result_states_have_recovery_actions(): # Labels shown in the empty state, plus the shared styling hook. assert "Clear search" in html assert "Switch filter to all" in html + assert "Switch profile to core" in html assert "view-empty-link" in html # Buttons are defensively typed so they never submit a surrounding form. assert "type = 'button'" in html