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:
1 parent
c494bf4111
commit
87b6811adf
6 files changed
+134
-14
No files matched your search
+35
-9
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user