Operator DevTools (custom domain shop.printableprompts.com) showed the
tell: bulk-tagger actions did a DOCUMENT POST -> 302 -> 200 and the
page rendered the SERVER-SIDE flash banner. That banner only survives
if the view took the non-AJAX HTTPFound branch — i.e. is_ajax() was
False: the app never saw X-Requested-With. Custom-domain shops sit
behind a Caddy reverse proxy that was not forwarding that request
header to uWSGI, so the capability-driven split ALWAYS chose 302 and
the page full-reloaded. Canonical host worked, so it looked fine.
- views/__init__.py:is_ajax() now returns True for
X-Requested-With == XMLHttpRequest OR request param ajax=1. The param
rides in the URL/body — no proxy strips it. Header kept for back-compat.
- tag_bulk.js (submitForm/doReorder/persistOrder FormData, fetchFocus
URL) and product_tags.js (post helper) now send ajax=1.
- Hardened tag_bulk.js: ZERO code paths full-reload on failure anymore.
reportFailure() surfaces HTTP status + content-type + body snippet as
a visible banner (the old form.submit()/location fallbacks turned
every server hiccup into 'the screen keeps refreshing' and hid the
cause). safeInit() + window 'error' handler make a dead script
visible (transient '✓ Tag editor interactive' proof-of-life banner)
instead of failing silently.
- Tests: +test_ajax_param_signals_ajax_without_header,
+test_no_ajax_signal_still_redirects,
+test_ajax_focus_via_param_returns_json. 1134 passed.
Docs: mps-24.md Phase 2.8d, CLAUDE.md (is_ajax dual signal).