Files
MarekWoandClaude Opus 5 59ef0dd6be fix(ui): explain a non-JSON answer instead of printing its parse error
An instance published behind an access list or Cloudflare Zero Trust gets
an HTML login or error page in place of JSON once that session expires.
`resp.json()` then throws "Unexpected token '<', "<!DOCTYPE "...", and the
repeater panes printed that at the user. Diagnosed on the production
server 2026-09-08: the origin had answered a clean 504 JSON ("No reply
from repeater within 10s") and nginx passed it through untouched, so the
HTML could only have come from in front of it.

New window.fetchJson() (fetch-utils.js, loaded from _head_i18n.html so all
eight entry points get it) returns the parsed body — `data.success` and
`data.error` checks are unchanged — and reports anything else in the same
shape the API uses: {success:false, error, http_status, non_json|offline}.
A 401/403 with a non-JSON body is named as an expired login, since that is
the case the user can act on; an error the API sent as JSON keeps its own
message. 88 call sites converted; those that deliberately inspect the
response (resp.ok/status, blob, uploads) still use fetch directly.

Also stops three catch blocks in the repeater panes from rendering the raw
exception text.

Verified in a browser: normal calls unchanged, an HTML answer comes back
as a readable sentence, a refused connection as a network error, and all
seven pages load with no console errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:47:31 +02:00

27 lines
1.4 KiB
HTML

{# Translation catalog + runtime.
Both must block: window.t has to exist before any inline handler or page script runs,
so never add defer/async. Include this in every <head>, immediately after the theme
pre-apply IIFE, giving a uniform ordering: theme -> i18n -> everything else.
The catalog URL is content-hashed and served immutable, so all 8 entry points
(including the 6 iframes) share a single cached fetch. #}
<script src="{{ i18n_catalog_url }}"></script>
<script src="{{ url_for('static', filename='js/i18n-runtime.js') }}"></script>
<script src="{{ url_for('static', filename='js/datetime-utils.js') }}"></script>
<script src="{{ url_for('static', filename='js/clipboard-utils.js') }}"></script>
{# fetchJson() needs window.t, which the two scripts above have already set up. #}
<script src="{{ url_for('static', filename='js/fetch-utils.js') }}"></script>
{# Demo mode. Injected here rather than through MC_CONFIG because only index.html
and dm.html define that object, and the console — the page most in need of
locking — is not one of them. Must be set before demo-lock.js runs. #}
<script>
window.MC_DEMO = {
enabled: {{ 'true' if demo_mode else 'false' }},
unlocked: {{ 'true' if demo_unlocked else 'false' }},
hasUnlockCode: {{ 'true' if demo_has_unlock_code else 'false' }}
};
</script>
<script src="{{ url_for('static', filename='js/demo-lock.js') }}"></script>