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 @@ -33,3 +33,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-07-24 - [Context-Safe Async Button States]

**Learning:** For asynchronous action buttons in control views (e.g., Browser navigate, reload, or close actions), passing the calling element to JavaScript using `this` (e.g., `onclick="browserNavigate(this)"`) allows contextual loading feedback and disabled states to be applied directly. This prevents double-submits or redundant in-flight network requests during longer operations without expensive DOM element lookups.
**Action:** Ensure asynchronous button handlers support passing and handling `this` to maintain context-safe button states and clear screen-reader feedback via `aria-busy`.
Comment on lines +37 to +40

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Laat de nieuwe journal-entry aan de repositoryregels voldoen.

Schrijf de nieuwe sectie in het Nederlands. Voeg een lege regel toe na de heading op Line 32. Static analysis meldt hiervoor markdownlint MD022.

As per coding guidelines: **/*: Communiceer in het Nederlands tenzij de gebruiker expliciet om Engels vraagt.

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 32-32: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 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 @.Jules/palette.md around lines 32 - 34, Werk de nieuwe sectie in
.Jules/palette.md bij naar het Nederlands en voeg direct na de heading een lege
regel toe, zodat de opmaak voldoet aan markdownlint MD022. Behoud de bestaande
inhoud en structuur verder ongewijzigd.

Sources: Coding guidelines, Linters/SAST tools

4 changes: 2 additions & 2 deletions .agents/eval/scorecard.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"ts": "2026-07-30T10:44:12Z",
"ts": "2026-07-31T14:33:24Z",
"pass": true,
"plane": "agent-taste",
"metrics": {
"taste_drift": 0,
"rule_count": 10,
"open_critical_signals": 0,
"open_signals": 0,
"days_since_score_refresh": 0.0,
"days_since_score_refresh": 1.16,
"taste_log_entries": 0
},
"failures": [],
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ jobs:
# (see the coverage job, with instrumentation) completes in ~185s.
- name: Test
run: >-
timeout 300s uv run pytest -vv --tb=short -ra
timeout 450s uv run pytest -v --tb=short -ra
--ignore=tests/test_computer_acceptance_e2e.py

# ── integration: REST, OAuth PKCE, API-key, admin-key, storage, proxy ──
Expand Down
52 changes: 45 additions & 7 deletions src/kater/web/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -1387,12 +1387,13 @@
<div class="browser-toolbar">
<input class="browser-url" id="browser-url" type="url"
placeholder="https://…" autocomplete="off" aria-label="Browser URL"
onkeydown="if(event.key==='Enter'){event.preventDefault();browserNavigate();}">
<button class="mini-btn interactive" type="button" onclick="browserNavigate()"
onkeydown="if(event.key==='Enter'){event.preventDefault();browserNavigate(document.getElementById('browser-go'));}">
<button class="mini-btn interactive" type="button" id="browser-go"
onclick="browserNavigate(this)"
aria-label="Navigate">Go</button>
<button class="mini-btn interactive" type="button" onclick="browserReload()"
<button class="mini-btn interactive" type="button" onclick="browserReload(this)"
aria-label="Reload page">Reload</button>
<button class="mini-btn interactive" type="button" onclick="closeBrowserSession()"
<button class="mini-btn interactive" type="button" onclick="closeBrowserSession(this)"
aria-label="Close session">Close</button>
</div>
<div class="browser-stage" id="browser-stage">
Expand Down Expand Up @@ -3892,6 +3893,7 @@ class ApiError extends Error {
let browserSelectedId = null;
let browserPollTimer = null;
let browserShotSeq = 0;
let browserNavigating = false;
const browserActionLog = new Map(); // session_id -> [{kind, ok, detail, ts}]

function stopBrowserPoll() {
Expand Down Expand Up @@ -4078,9 +4080,14 @@ class ApiError extends Error {
}
}

async function closeBrowserSession() {
async function closeBrowserSession(btn) {
if (!browserSelectedId) { toast('no session selected', 'error'); return; }
const id = browserSelectedId;
if (btn) {
btn.disabled = true;
btn.setAttribute('aria-busy', 'true');
btn.textContent = 'Closing...';

@coderabbitai coderabbitai Bot Jul 31, 2026

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Gebruik de vereiste ellipses in de laadlabels.

De PR-doelstelling noemt Closing…, Go… en Reloading…. De handlers gebruiken drie ASCII-punten. Gebruik het Unicode-ellipsisteken in alle drie labels.

Voorgestelde wijziging
-    btn.textContent = 'Closing...';
+    btn.textContent = 'Closing…';
-    btn.textContent = 'Go...';
+    btn.textContent = 'Go…';
-    btn.textContent = 'Reloading...';
+    btn.textContent = 'Reloading…';

Also applies to: 4063-4063, 4093-4093

🤖 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` at line 4034, Update the loading-label text
assignments in the relevant handlers to use the Unicode ellipsis character:
change the labels to “Closing…”, “Go…”, and “Reloading…” instead of three ASCII
periods, including the assignments near the referenced btn.textContent
statements.

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 PR description actually uses ASCII "Go...", "Reloading...", "Closing...", and the surrounding dashboard handlers use the same ASCII form ("Saving...", "Enabling...", "Merging..."), so switching only these three to U+2026 would break local consistency.

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.

Skipped: comment is from another GitHub bot.

}
try {
await apiDelete('/api/browser/sessions/' + encodeURIComponent(id));
toast('session closed');
Expand All @@ -4091,14 +4098,27 @@ class ApiError extends Error {
await loadBrowserView();
} catch (e) {
toast('close: ' + (e.message || 'failed'), 'error');
} finally {
if (btn) {
btn.disabled = false;
btn.removeAttribute('aria-busy');
btn.textContent = 'Close';
}
}
}

async function browserNavigate() {
async function browserNavigate(btn) {
if (browserNavigating) return;
if (!browserSelectedId) { toast('no session selected', 'error'); return; }
const urlEl = document.getElementById('browser-url');
const url = urlEl ? urlEl.value.trim() : '';
if (!url) { toast('enter a URL', 'error'); return; }
browserNavigating = true;
if (btn) {
btn.disabled = true;
btn.setAttribute('aria-busy', 'true');
btn.textContent = 'Go...';
}
try {
const data = await apiPost(
'/api/browser/sessions/' + encodeURIComponent(browserSelectedId) + '/act',
Expand All @@ -4113,11 +4133,23 @@ class ApiError extends Error {
} catch (e) {
pushBrowserLog(browserSelectedId, { kind: 'navigate', ok: false, detail: e.message || 'failed' });
toast('navigate: ' + (e.message || 'failed'), 'error');
} finally {
browserNavigating = false;
if (btn) {
btn.disabled = false;
btn.removeAttribute('aria-busy');
btn.textContent = 'Go';
}
}
}

async function browserReload() {
async function browserReload(btn) {
if (!browserSelectedId) { toast('no session selected', 'error'); return; }
if (btn) {
btn.disabled = true;
btn.setAttribute('aria-busy', 'true');
btn.textContent = 'Reloading...';
}
try {
const data = await apiPost(
'/api/browser/sessions/' + encodeURIComponent(browserSelectedId) + '/act',
Expand All @@ -4131,6 +4163,12 @@ class ApiError extends Error {
} catch (e) {
pushBrowserLog(browserSelectedId, { kind: 'reload', ok: false, detail: e.message || 'failed' });
toast('reload: ' + (e.message || 'failed'), 'error');
} finally {
if (btn) {
btn.disabled = false;
btn.removeAttribute('aria-busy');
btn.textContent = 'Reload';
}
}
}

Expand Down
9 changes: 7 additions & 2 deletions tests/test_ci_workflow_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
only run inside GitHub Actions.
"""

import re
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
Expand Down Expand Up @@ -39,8 +40,12 @@ def test_ci_jobs_install_the_browser_extra() -> None:
def test_unit_matrix_job_uses_kater_checkout_sha_and_longer_timeout() -> None:
block = _job_block(CI.read_text(encoding="utf-8"), "unit", "integration")
assert KATER_CHECKOUT_SHA in block
assert "timeout 300s uv run pytest" in block
assert "timeout 180s" not in block
# De exacte limiet groeit mee met de suite; controleer alleen dat de
# interne bewaking bestaat en ruim boven de ~180s-limiet blijft die
# eerder op Python 3.14 werd overschreden.
match = re.search(r"timeout (\d+)s uv run pytest", block)
assert match is not None
assert int(match.group(1)) > 180


def test_computer_acceptance_checks_out_kater_and_the_private_runtime() -> None:
Expand Down
140 changes: 140 additions & 0 deletions tests/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -789,3 +789,143 @@ def test_credentials_modal_focus_restoration_behavior_node(tmp_path):
assert res["body"]["capturedNothing"] is True
assert res["body"]["cleared"] is True
assert res["body"]["focusRestored"] is False


def _js_handler_block(html: str, signature: str) -> str:
start = html.index(signature)
end = html.index("\nasync function", start + len(signature))
return html[start:end]


@pytest.mark.parametrize(
("handler", "loading_label", "idle_label"),
[
("closeBrowserSession", "Closing...", "Close"),
("browserNavigate", "Go...", "Go"),
("browserReload", "Reloading...", "Reload"),
],
)
def test_browser_view_buttons_use_context_loading_states(handler, loading_label, idle_label):
html = render_dashboard()
assert f'onclick="{handler}(this)"' in html

block = _js_handler_block(html, f"async function {handler}(btn)")
assert "btn.disabled = true" in block
assert "btn.setAttribute('aria-busy', 'true')" in block
assert f"btn.textContent = '{loading_label}'" in block

restore = block[block.index("} finally {") :]
assert "btn.disabled = false" in restore
assert "btn.removeAttribute('aria-busy')" in restore
assert f"btn.textContent = '{idle_label}'" in restore


def test_browser_url_enter_serializes_navigation_through_go_button():
html = render_dashboard()
assert 'id="browser-go"' in html
assert "browserNavigate(document.getElementById('browser-go'))" in html

block = _js_handler_block(html, "async function browserNavigate(btn)")
assert "if (browserNavigating) return;" in block
assert "browserNavigating = true;" in block
assert "browserNavigating = false;" in block[block.index("} finally {") :]
Comment thread
coderabbitai[bot] marked this conversation as resolved.


# Gedragstest voor de navigatie-guard: voert het echte `browserNavigate` uit in
# Node met een vertraagde `apiPost`, zodat een tweede aanroep tijdens een
# lopende navigatie aantoonbaar wordt genegeerd in plaats van alleen de
# brontekst van de guard te matchen.
_BROWSER_NAV_HARNESS = r"""
const urlEl = { value: 'https://example.com' };
const document = {
getElementById(id) { return id === 'browser-url' ? urlEl : null; },
};
let browserNavigating = false;
let browserSelectedId = 'sess-1';
function toast() {}
function pushBrowserLog() {}
function showBrowserShot() {}
async function pollBrowserScreenshot() {}
async function loadBrowserView() {}

let apiPostCalls = 0;
let pendingResolve = null;
function apiPost() {
apiPostCalls += 1;
return new Promise((resolve) => { pendingResolve = resolve; });
}

/*__DASHBOARD_JS__*/

const btn = {
disabled: false,
textContent: 'Go',
attrs: {},
setAttribute(k, v) { this.attrs[k] = v; },
removeAttribute(k) { delete this.attrs[k]; },
};

(async () => {
const first = browserNavigate(btn);
const duringFlight = {
disabled: btn.disabled,
ariaBusy: btn.attrs['aria-busy'] === 'true',
label: btn.textContent,
};

// Tweede aanroep terwijl de eerste nog loopt: de guard moet die laten vallen.
await browserNavigate(btn);
const callsWhilePending = apiPostCalls;

pendingResolve({ ok: true, url: 'https://example.com', screenshot_b64: 'x' });
await first;
const afterFlight = {
disabled: btn.disabled,
ariaBusy: 'aria-busy' in btn.attrs,
label: btn.textContent,
};

// Na afronding laat de guard een nieuwe navigatie weer door.
const second = browserNavigate(btn);
const callsAfterRelease = apiPostCalls;
pendingResolve({ ok: true, url: 'https://example.com', screenshot_b64: 'x' });
await second;

process.stdout.write(JSON.stringify({
duringFlight,
callsWhilePending,
afterFlight,
callsAfterRelease,
}));
})().catch((err) => {
console.error(err);
process.exit(1);
});
"""


def test_browser_navigate_drops_overlapping_invocations_node(tmp_path):
node = shutil.which("node") or shutil.which("nodejs")
if node is None:
pytest.skip("node is required to execute the dashboard JS")
assert node is not None
html = render_dashboard()
dashboard_js = _extract_js_function(html, "browserNavigate")
script = tmp_path / "browser_navigate_guard.cjs"
script.write_text(
_BROWSER_NAV_HARNESS.replace("/*__DASHBOARD_JS__*/", dashboard_js),
encoding="utf-8",
)
proc = subprocess.run(
[node, str(script)], capture_output=True, text=True, timeout=60, check=False
)
assert proc.returncode == 0, proc.stderr
res = json.loads(proc.stdout)
# Tijdens de eerste navigatie is de knop uitgeschakeld en bezig gemarkeerd.
assert res["duringFlight"] == {"disabled": True, "ariaBusy": True, "label": "Go..."}
# De overlappende tweede aanroep bereikt de API niet.
assert res["callsWhilePending"] == 1
# Na afloop is de knopstatus hersteld.
assert res["afterFlight"] == {"disabled": False, "ariaBusy": False, "label": "Go"}
# En een volgende navigatie mag weer door de guard.
assert res["callsAfterRelease"] == 2
Loading