Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
42 changes: 40 additions & 2 deletions src/kater/web/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: You're hand-crafting buttons here like it's 1995, but two blocks over the Catalog section already has an addLink helper that does the exact same thing. This is like building a bicycle from scratch when there's a perfectly good car parked in the driveway.

🩹 The Fix: Extract the button-creation pattern into a shared helper (or reuse the Catalog's addLink pattern) so all three views stay in sync when the button style evolves.

📏 Severity: suggestion


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: Another hand-built button where the Catalog already handed you the tool. This is the third time you've reinvented the same wheel in this PR — at this rate you'll have your own tire factory by the end of the week.

🩹 The Fix: Reuse the same helper pattern from the Catalog section (or extract one) so the Fabric view's empty-state button stays consistent with the rest of the codebase.

📏 Severity: suggestion


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

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);
}
Comment on lines +4236 to +4243

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Geef het profiel door en laad Fabric opnieuw na de actie.

loadFabricView() vraagt op Line 4215 /api/capabilities op zonder profile. src/kater/api/fabric_routes.py valt dan terug op core. De nieuwe lege toestand kan daardoor niet betrouwbaar het actieve profiel tonen.

Daarnaast laadt switchProfile() de Fabric-view niet opnieuw. Na een klik blijft de oude inhoud staan.

Voorgestelde wijziging
 async function switchProfile(p) {
   ...
   loadCatalog();
   if (currentView === 'catalog') loadCatalogView();
+  if (currentView === 'fabric') loadFabricView();
 }

 async function loadFabricView() {
   ...
-    api('/api/capabilities'),
+    api('/api/capabilities?profile=' + encodeURIComponent(activeProfile || 'core')),
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/kater/web/dashboard.py` around lines 4236 - 4243, Update loadFabricView()
to include the current active profile when requesting /api/capabilities, so the
response reflects that profile instead of defaulting to core. Update
switchProfile() to reload the Fabric view after changing profiles, ensuring the
displayed content and empty-state action reflect the new profile.

capsEl.appendChild(empty);
} else {
for (const item of caps.slice(0, 50)) {
Expand Down
1 change: 1 addition & 0 deletions tests/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: You're testing that the string "Switch profile to core" exists in the HTML, but since activeProfile defaults to 'core', that button would never actually render at runtime. It's like testing that a fire extinguisher exists in the building by checking the blueprint — the extinguisher might as well not exist.

🩹 The Fix: Either set activeProfile to a non-core value in the test and verify the button appears, or test the conditional rendering logic directly instead of just checking for the string in the source.

📏 Severity: suggestion


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already covered: a behavioral Node/DOM test (test_server_map_empty_state_recovery_buttons_behavior_node) was added in a prior commit that sets activeProfile to a non-core value, verifies the button renders, and verifies clicking it switches the profile to core.

assert "view-empty-link" in html
# Buttons are defensively typed so they never submit a surrounding form.
assert "type = 'button'" in html
Expand Down
Loading