v3.28.7: the batch panel freeze

ramBatchFetch's re-entrancy guard (_ramBatchFetching) is only cleared
when the fetch settles, and ramBatchApplyStatus returned without
rendering whenever it decided a fetch was due - so one stalled
/api/ram-batch/status request (the browser's fetch has no timeout) froze
the whole panel (counts, phase, notice, throbber) at the last successful
render while the server kept installing (live 2026-10-14: 31 installs,
the panel stuck at "Installed: 12"; the log showed all 31 attempts and a
healthy /api/status cadence, so the data was there).

- The summary render now runs on every poll before the fetch decision:
  the headline (phase, current card, counts, notice) always follows the
  2 s /api/status poll; the detailed fetch only adds the attempts table.
- A fetch guard older than RAM_BATCH_FETCH_STALE_MS (10 s) is released,
  so a stalled fetch cannot block the report forever - the next poll
  retries and the attempts table recovers.

Reproduced with the page's real functions: 12 good fetches, the 13th hung
-> the panel stuck at 12 (the reported number); with the fix it follows
the summary to 31 and the stale guard retries.  Tests: a hung fetch keeps
the counts following the summary; a stale guard is released and retried
(a fresh one is kept).  919 frontend + 928 Python; sw cache simple-v445.
This commit is contained in:
2026-10-14 23:20:34 +03:00
parent 408ad87a0e
commit 269e507580
5 changed files with 90 additions and 7 deletions
+15 -3
View File
@@ -1910,7 +1910,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.28.6';
const SIMPLE_VERSION = '3.28.7';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
@@ -10155,8 +10155,11 @@ function ramHideProgress() {
// (the attempts list) is fetched only when the summary says it changed.
let _ramBatchLast = null; // the last detailed /api/ram-batch/status
let _ramBatchFetching = false;
let _ramBatchFetchAt = 0; // when the in-flight fetch started (the watchdog)
let _ramBatchSummary = null; // /api/status.ram_batch
let _ramBatchProgress = null; // /api/status.ram_progress
// A fetch that never settled must not block the report forever (v3.28.7).
const RAM_BATCH_FETCH_STALE_MS = 10000;
function ramBatchPanelVisible() {
const op = document.getElementById('ram-op');
@@ -10167,6 +10170,7 @@ function ramBatchPanelVisible() {
async function ramBatchFetch() {
if (_ramBatchFetching) return;
_ramBatchFetching = true;
_ramBatchFetchAt = Date.now();
try {
_ramBatchLast = await pysimFetch('/api/ram-batch/status');
} catch (e) { /* the next poll retries */ }
@@ -10181,14 +10185,22 @@ function ramBatchApplyStatus(status) {
_ramBatchSummary = status.ram_batch || null;
_ramBatchProgress = status.ram_progress || null;
if (!ramBatchPanelVisible()) return;
// The summary carries the headline (the phase, the current card, the
// counts, the notice): render it on every poll, even when a detailed fetch
// is due or stuck - a hung fetch used to freeze the whole panel at the
// last successful render (v3.28.7).
ramBatchRender();
if (_ramBatchFetching && Date.now() - _ramBatchFetchAt > RAM_BATCH_FETCH_STALE_MS) {
// the in-flight fetch is stale (a stalled connection): release the
// guard so this poll retries and the report can recover
_ramBatchFetching = false;
}
const total = _ramBatchSummary ? (_ramBatchSummary.attempts_total || 0) : 0;
const active = _ramBatchSummary ? !!_ramBatchSummary.active : false;
if (!_ramBatchLast || (_ramBatchLast.attempts_total || 0) !== total
|| !!_ramBatchLast.active !== active) {
ramBatchFetch();
return;
}
ramBatchRender();
}
function ramBatchRender() {