Files view: surface fetch errors instead of "no operations" (v2.3.1)

- `API._fetch` never checked the HTTP status: an error body (e.g. the 404
  `{"error": "Session not found"}`) was returned as data, so the Files view
  rendered it as "No file operations recorded" — and, worse, that bad payload
  was cached per session id, which is why only re-opening the session (which
  resets the cache) appeared to fix it.  It now throws with the server's
  message for any non-OK response (all 13 `await API.*` call sites are inside
  try/catch) and marks the server unavailable only for a real network error
- `renderFileView` accepts a payload only when it carries a `timeline` array:
  a genuine `timeline_total === 0` keeps the empty state, anything else shows
  "Failed to load files: <reason>"; failures are not cached, so the next visit
  retries; a response that arrives after the user switched sessions is ignored
- tests: HTTP-error vs network-error behaviour of `_fetch` (network error is
  the only disconnected state), the valid/empty/error render paths and the
  retry-after-error, the stale-response guard
- v2.3.1; SW cache simtrace-analyser-v64
This commit is contained in:
catarrh committed 2026-10-17 21:04:03 +03:00
1 parent c494bf4111
commit 87b6811adf
6 files changed
+134 -14

No files matched your search

+35 -9
View File
@@ -1161,14 +1161,28 @@ const API = {
opts.headers['Content-Type'] = 'application/json';
opts.body = JSON.stringify(body);
}
let res;
try {
const res = await fetch(API_BASE() + path, opts);
return await res.json();
res = await fetch(API_BASE() + path, opts);
} catch (e) {
// The server is unreachable (network error) — that *is* a
// disconnected state.
STATE.serverAvailable = false;
updateCaptureButton();
throw e;
}
let data = {};
try {
data = await res.json();
} catch (e) { /* non-JSON body */ }
if (!res.ok) {
// The server answers errors as {"error": …}; surface them instead
// of letting callers treat the error body as data.
throw Object.assign(
new Error((data && data.error) || `HTTP ${res.status}`),
{ status: res.status, code: data && data.code });
}
return data;
},
status() { return this._fetch('GET', '/api/status'); },
listSessions() { return this._fetch('GET', '/api/sessions'); },
@@ -2105,19 +2119,31 @@ async function renderFileView() {
view.innerHTML = `<div class="empty-state" style="height:auto;padding:24px;">${t('Open a saved session from the Sessions tab.')}</div>`;
return;
}
if (STATE.filesData === null || STATE.filesSessionId !== STATE.sessionViewId) {
const sid = STATE.sessionViewId;
let data = STATE.filesData;
// A failure is never cached: only a well-formed payload is remembered, so
// the next visit retries instead of showing a stale error.
if (data === null || STATE.filesSessionId !== sid ||
!Array.isArray(data.timeline)) {
view.innerHTML = `<div class="empty-state" style="height:auto;padding:24px;">${t('Loading…')}</div>`;
try {
STATE.filesData = await API.getSessionFiles(STATE.sessionViewId);
STATE.filesSessionId = STATE.sessionViewId;
const payload = await API.getSessionFiles(sid);
if (STATE.sessionViewId !== sid) return; // the view moved on
if (!payload || !Array.isArray(payload.timeline)) {
throw new Error((payload && payload.error) || t('Failed to load files'));
}
data = payload;
STATE.filesData = payload;
STATE.filesSessionId = sid;
} catch (e) {
if (STATE.sessionViewId !== sid) return;
STATE.filesData = null;
view.innerHTML = `<div class="empty-state" style="height:auto;padding:24px;">${t('Failed to load files')}</div>`;
STATE.filesSessionId = null;
view.innerHTML = `<div class="empty-state" style="height:auto;padding:24px;">${t('Failed to load files')}: ${escapeHtml((e && e.message) || '')}</div>`;
return;
}
}
const data = STATE.filesData;
const timeline = (data && data.timeline) || [];
const timeline = data.timeline;
if (!timeline.length) {
view.innerHTML = `<div class="empty-state" style="height:auto;padding:24px;">${t('No file operations recorded')}</div>`;
return;
@@ -3046,7 +3072,7 @@ function onFilterChange() {
}
// ──────────────────── Versioning ────────────────────
const PWA_VERSION = '2.3.0';
const PWA_VERSION = '2.3.1';
const MIN_SERVER_VERSION = '2.3.0';
function parseVersion(v) {