fix: drop stale STK dialogs, finish pending on TP re-send, mute client-gone (v3.6.35)

The live log showed the "stuck" state: after the menu watchdog (60 s)
answered a pending command, the PWA kept the panel; the next click got
{'error': 'no pending command'}, 400 and that reply was fed to the dialog
handler as if it were a card answer.  A page reload then cancelled in-flight
polls, flooding the log with BrokenPipeError tracebacks.

- stkMenuRespond detects the already-answered reply (stkNoPendingError) and
  closes the panel (stkMenuPanelReset) instead of treating it as an answer;
  the 5 s status poll drops the panel whenever the server reports
  pending: false, even when the Phone tab is hidden.
- /api/terminal-profile (POST) and /api/rescue finish a pending command
  with a cancel TR first, so re-sending the TP can never shadow an
  unanswered FETCH.
- _send_json and _serve_static swallow BrokenPipe/ConnectionReset/
  ConnectionAborted with one "CLIENT GONE" line (a reload cancels polls);
  the handlers' state changes still stand.

Tests: stkNoPendingError + the already-answered respond path; version
3.6.35; sw simple-v308; stylesheet rebuilt.
This commit is contained in:
2026-09-29 00:37:56 +03:00
parent 0b7b6b8157
commit bd5dc69c4e
5 changed files with 67 additions and 8 deletions
+19 -1
View File
@@ -22,6 +22,8 @@ function extractFunc(src, name, asyncFn) {
}
let code = extractFunc(html, 'stkMenuRespond', true) + '\n';
code += extractFunc(html, 'stkNoPendingError') + '\n';
code += extractFunc(html, 'stkMenuPanelReset') + '\n';
code += extractFunc(html, 'stkMenuNaiSuffix') + '\n';
code += extractFunc(html, 'stkMenuItemsHtml') + '\n';
code += 'globalThis.esc = s => s;\n';
@@ -32,10 +34,12 @@ function setup(response) {
globalThis.pysimFetch = async () => response;
globalThis.stkMenuHandleResponse = d => { calls.handled = d; };
globalThis.stkMenuRenderItems = () => { calls.rendered++; };
globalThis.stkCheckMenu = () => {};
const btns = { classList: { add: () => {} } };
const back = { style: {} };
globalThis.document = {
getElementById: id => (id === 'stk-menu-buttons' ? btns : id === 'stk-back-btn' ? back : { innerHTML: '' }),
getElementById: id => (id === 'stk-menu-buttons' ? btns : id === 'stk-back-btn' ? back
: { innerHTML: '', classList: { add: () => {} } }),
};
return calls;
}
@@ -114,3 +118,17 @@ test('ok navigates with the server response', async () => {
assert.strictEqual(calls.handled, data);
assert.strictEqual(calls.rendered, 0);
});
test('stkNoPendingError detects the already-answered reply', () => {
assert.strictEqual(stkNoPendingError({ error: 'no pending command' }), true);
assert.strictEqual(stkNoPendingError({ sw: '9000', type: 'done' }), false);
assert.strictEqual(stkNoPendingError({ error: 'other' }), false);
assert.strictEqual(stkNoPendingError(null), false);
});
test('an already-answered respond does not reach the dialog handler', async () => {
const calls = setup({ error: 'no pending command' });
await stkMenuRespond('ok');
assert.strictEqual(calls.handled, null);
assert.strictEqual(calls.rendered, 0);
});