doc 19: english-only lockdown audit (read-only baseline)
Cross-layer audit supplementing docs 15 and 16. Confirms doc-15 root cause still live (8/8 users have UICulture absent; force-english script unrun), enumerates 93 served locale chunks (de-json contains 'Abspielen'), and proposes 4-pronged remediation: per-user POST + wrapper patch + Traefik Accept-Language rewrite + navigator.language shim. Read-only. No Jellyfin mutations performed.
This commit is contained in:
parent
d5d68563d2
commit
a3f82dfcc0
1 changed files with 347 additions and 0 deletions
347
docs/19-english-only-audit.md
Normal file
347
docs/19-english-only-audit.md
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
# 19 - English-Only Lockdown Audit (Read-Only Baseline)
|
||||
|
||||
> Owner saw the Play button render as **"Abspielen"** (German). Goal:
|
||||
> "everything English only, remove the ability to be in another language at
|
||||
> all". This doc supplements `docs/15-force-english.md` and `docs/16-jellyfin-branding-leaks.md` —
|
||||
> it is the cross-layer baseline for the lockdown branch.
|
||||
|
||||
Audited: 2026-05-08 against live `https://arrflix.s8n.ru`, Jellyfin 10.10.3.
|
||||
Auditor: s8n. Mode: read-only. No POST/PATCH/PUT to Jellyfin, no file
|
||||
modifications outside this doc.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR — root cause + why doc 15 didn't close it
|
||||
|
||||
1. **Per-user `Configuration.UICulture` is still absent on every account.**
|
||||
All 8 users return `Configuration.UICulture` as a missing key (verified
|
||||
live 2026-05-08, see Per-User Table below). Doc 15 correctly identified
|
||||
the fix and shipped the `bin/force-english-all-users.sh` script — but
|
||||
**the script was never executed**. There is no audit trail of a
|
||||
`204 No Content` against `/Users/{id}/Configuration` in the activity log
|
||||
for any user, and the live state proves it (UICulture still absent on
|
||||
all 8). When `UICulture` is absent, the SPA falls back to
|
||||
`navigator.language` / `Accept-Language`, so any browser sending `de-*`
|
||||
loads the German bundle and renders "Abspielen". This is layer (5) in
|
||||
the table below.
|
||||
|
||||
2. **The German translation bundle is shipped and live.** `de-json.1afccc006ab8bb6c5953.chunk.js`
|
||||
is reachable, returns HTTP 200, and contains `"Play":"Abspielen"`,
|
||||
`"Settings":"Einstellungen"`, `"Save":"Speichern"`, etc. — 1963 unique
|
||||
translated keys. 92 other locale chunks ship alongside it. Until those
|
||||
are removed from the served bundle, the SPA can always select a
|
||||
non-English locale even if every user has `UICulture=en-US` (e.g. a new
|
||||
user who never authenticated, or a tampered SPA). Doc 15 explicitly
|
||||
noted "no server flag forces SPA to ignore Accept-Language" but stopped
|
||||
at the per-user pin — it didn't propose deleting the bundles.
|
||||
|
||||
In two sentences: **The Play button renders "Abspielen" because every user
|
||||
has `Configuration.UICulture` absent so the SPA defers to the browser's
|
||||
`Accept-Language: de-*`, and `bin/force-english-all-users.sh` (the doc-15
|
||||
fix) was authored but never run. Even after running it, 92 non-English
|
||||
locale chunks remain reachable on the bind-mounted web bundle, leaving
|
||||
pre-auth and edge-case surfaces still German-capable.**
|
||||
|
||||
---
|
||||
|
||||
## Per-Layer Findings
|
||||
|
||||
| # | Layer | Current Value | Desired | How to Fix | Owner |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | Server `/System/Configuration.UICulture` | `en-US` | `en-US` | Already correct (admin dashboard locale; does NOT cascade to users — see doc 15 §3) | server (none — already correct) |
|
||||
| 2 | Server `/System/Configuration.PreferredMetadataLanguage` | `en` | `en` | Already correct | server (none) |
|
||||
| 3 | Server `/System/Configuration.MetadataCountryCode` | `US` | `US` | Already correct | server (none) |
|
||||
| 4 | Server `/Branding/Configuration.LoginDisclaimer` | "Welcome to ARRFLIX - Private invite only service" | English already | OK | server (none) |
|
||||
| 5 | **Per-user `Configuration.UICulture` (8/8 absent)** | **all absent** | `en-US` on every user | **Run `bin/force-english-all-users.sh`** with admin token; idempotent. Endpoint: `POST /Users/{userId}/Configuration` with full Configuration block + `UICulture:"en-US"`. | **server agent — primary fix** |
|
||||
| 6 | Per-user `Configuration.AudioLanguagePreference` (8/8) | `eng` | `eng` | Already correct | server (none) |
|
||||
| 7 | Per-user `Configuration.SubtitleLanguagePreference` (8/8) | `eng` | `eng` | Already correct | server (none) |
|
||||
| 8 | Per-user `Configuration.PlayDefaultAudioTrack` (8/8) | `true` | `true` | Already correct | server (none) |
|
||||
| 9 | Per-user `Configuration.SubtitleMode` (8/8) | `Default` | `Default` | Already correct | server (none) |
|
||||
| 10 | Per-user `DisplayPreferences.CustomPrefs.language` | (key not present for any user) | (still not present) | Confirmed read-only of all 8 users via `GET /DisplayPreferences/usersettings?userId=...&client=emby` — no `language` key in `CustomPrefs`. Locale is NOT stored here. Layer is non-issue. | none |
|
||||
| 11 | Plugin-shipped UI strings | 6 plugins (AudioDB, MusicBrainz, OMDb, Open Subtitles, Studio Images, TMDb); none ship locale UI strings | None | No action — these are metadata-source plugins, not UI string sources. | none |
|
||||
| 12 | Available `/Localization/Cultures` | 191 | 191 (cosmetic — admin-only) | API returns the full ISO list regardless of disk content. Cannot be trimmed via API. Admin-only. Defer. | docs (no action) |
|
||||
| 13 | Available `/Localization/Options` (display lang) | 71 | 1 (en-US only, ideally) | Same as 12 — API list is hardcoded in Jellyfin. Cannot be trimmed via API. **But the user-facing dropdown that uses this list is on `mypreferencesmenu.html` which is already hidden by the inject-shim.** Non-issue for non-admins; admin keeps full list. | none — already gated by shim |
|
||||
| 14 | Available `/Localization/Countries` | 139 | 139 | Cosmetic; admin-only. No action. | none |
|
||||
| 15 | SPA `index.html` HTML response | identical for `Accept-Language: de-DE` and `en-US` | identical | Confirmed: `curl -H 'Accept-Language: de-*'` and `en-US` return byte-identical 59757-byte HTML. **Locale selection happens client-side in JS**, not server-side. So there is no server header rewrite to add. | web (none) |
|
||||
| 16 | **Web bundle locale chunks `<lang>-json.<hash>.chunk.js`** | **93 locale chunks served (de, fr, es, ru, zh-cn, ja, ko, ...)** including `de-json.1afccc006ab8bb6c5953.chunk.js` containing `"Play":"Abspielen"` | only `en-us-json.<hash>.chunk.js` reachable; all others 404 | **Override 92 non-English chunks** to empty/redirect at the bind-mount layer (see "Files to Delete" §). Compose pattern: bind-mount each as `:/jellyfin/jellyfin-web/<lang>-json.<hash>.chunk.js:ro` from a 1-byte `{}` stub. Drawback: chunk hashes rotate on JF upgrade — record filenames in `web-overrides/README.md` and re-pin after each image bump. **Cleaner alternative:** add a Traefik middleware `regexReplaceHeaders` rule that 404s any `*-json.*.chunk.js` whose lang prefix isn't `en-us`. | **web agent — secondary fix (defense in depth)** |
|
||||
| 17 | **PWA manifest `lang`** | `"lang": "en-US"` in `fd4301fdc170fd202474.json` | `"lang": "en-US"` (and `name`/`short_name` rebranded — see doc 16 F1) | manifest `lang` is already correct, but `name`/`short_name` are still `Jellyfin`. Folded into doc 16 F1, not duplicated here. | web (doc 16 work) |
|
||||
| 18 | Pre-auth splash bundle strings | reads `navigator.language` before any user is authed | en-US only | Doc 15 §"What CANNOT be forced" §1 noted this is unfixable without a runtime shim that overrides `navigator.language`. **NEW PROPOSAL:** patch `bin/inject-shim.py` to inject `Object.defineProperty(navigator, 'language', { value: 'en-US' }); Object.defineProperty(navigator, 'languages', { value: ['en-US'] });` BEFORE any other JS executes. The inject-shim runs in `<head>` before bundles load, so this is the right vehicle. | **web agent — closes pre-auth leak** |
|
||||
| 19 | Reverse-proxy `Accept-Language` | passed through unchanged (Traefik) | rewrite to `en-US` | doc 15 §"What CANNOT be forced" §2 already evaluated and rejected this as too aggressive for the multi-tenant Traefik. **Re-evaluation:** ARRFLIX is the only consumer of arrflix.s8n.ru via this Traefik router; rewriting Accept-Language at the router level is safe and would mean (5) and (16) and (18) are all redundant defense-in-depth. Add a `traefik.http.middlewares.arrflix-lang.headers.customrequestheaders.Accept-Language=en-US,en;q=0.9` middleware. | **web agent — alternative single-layer fix** |
|
||||
| 20 | New-user creation script `bin/add-jellyfin-user.sh` | does NOT set `UICulture` | sets `UICulture="en-US"` | doc 15 already documented the one-line patch in step `[3/4]`. Apply the diff. | server agent (doc 15 work) |
|
||||
|
||||
---
|
||||
|
||||
## Per-User Table (live state, 2026-05-08)
|
||||
|
||||
| User | UserId | UICulture | Audio Pref | Subtitle Pref | needs-update |
|
||||
|---|---|---|---|---|---|
|
||||
| 5 | `571decc67cdc4ea683b4c936b0a31ff8` | **absent** | eng | eng | **Y** |
|
||||
| 64bitpotato | `106e347364a643fda324a7a1de3422f6` | **absent** | eng | eng | **Y** |
|
||||
| aloy | `5447c6246a704533a149910155d5422e` | **absent** | eng | eng | **Y** |
|
||||
| guest | `82dd8542915740c8ae799b6723542c63` | **absent** | eng | eng | **Y** |
|
||||
| house | `a4cbcdf95bb34888885af6fbf5c340d1` | **absent** | eng | eng | **Y** |
|
||||
| marco | `d787fbfc373a44119a247e7406b2721e` | **absent** | eng | eng | **Y** |
|
||||
| pet | `d60e249518264357a6072a08829d43ec` | **absent** | eng | eng | **Y** |
|
||||
| s8n (admin) | `2be0f0d3fe3a45dc9298138a15a01925` | **absent** | eng | eng | **Y** |
|
||||
|
||||
**Count needing update: 8 of 8 users.** This is the entire active user
|
||||
roster. Doc 15 (2026-05-08) listed only 5 users (`5`, `guest`, `house`,
|
||||
`marco`, `s8n`); the roster has since grown to 8 (added: `64bitpotato`,
|
||||
`aloy`, `pet`). All 3 new users were created via `bin/add-jellyfin-user.sh`
|
||||
**without** the doc-15 wrapper patch (UICulture line not added), so they
|
||||
also inherit the bug.
|
||||
|
||||
---
|
||||
|
||||
## Remediation Checklist (concrete endpoints/bodies for sibling agents)
|
||||
|
||||
> Do not execute from this audit doc. Sibling agents own implementation.
|
||||
|
||||
### Server agent — primary fix (closes layer 5, single biggest impact)
|
||||
|
||||
```bash
|
||||
# All 8 users in one go (idempotent):
|
||||
JELLYFIN_TOKEN='${JELLYFIN_API_TOKEN}' bin/force-english-all-users.sh
|
||||
|
||||
# Spot-verify one user post-fix (expect "en-US"):
|
||||
curl -ks https://arrflix.s8n.ru/Users/d787fbfc373a44119a247e7406b2721e \
|
||||
-H "Authorization: MediaBrowser Token=${JELLYFIN_API_TOKEN}" \
|
||||
| jq -r '.Configuration.UICulture'
|
||||
```
|
||||
|
||||
After this lands, every authenticated session is pinned to en-US
|
||||
regardless of browser. Pre-auth and chunk-bundle leaks (16, 18) remain.
|
||||
|
||||
### Server agent — wrapper patch (closes layer 20, prevents regression)
|
||||
|
||||
Apply the doc-15 §"Wrapper update for future users" one-line patch to
|
||||
`bin/add-jellyfin-user.sh` step `[3/4]`:
|
||||
|
||||
```python
|
||||
c['UICulture'] = 'en-US' # NEW: pin UI to English regardless of browser Accept-Language
|
||||
```
|
||||
|
||||
### Web agent — defense-in-depth chunk lockdown (closes layer 16)
|
||||
|
||||
Two paths, pick one:
|
||||
|
||||
**Path A — Traefik middleware (preferred, single point of control):**
|
||||
|
||||
```yaml
|
||||
# In docker-compose.yml jellyfin labels:
|
||||
- "traefik.http.routers.jellyfin.middlewares=arrflix-lang"
|
||||
- "traefik.http.middlewares.arrflix-lang.headers.customrequestheaders.Accept-Language=en-US,en;q=0.9"
|
||||
```
|
||||
|
||||
Pros: one line, no bind-mounts to maintain, immune to JF upgrades.
|
||||
Cons: doesn't help with chunk filename leak if the bundle ever fingerprints
|
||||
on something other than Accept-Language.
|
||||
|
||||
**Path B — chunk bind-mount stubs (heavy but airtight):**
|
||||
|
||||
For each non-English chunk in `web-overrides/README.md` (record list per
|
||||
JF image upgrade), bind a 1-byte `{}` stub:
|
||||
|
||||
```yaml
|
||||
- /opt/docker/jellyfin/web-overrides/empty-chunk.js:/jellyfin/jellyfin-web/de-json.1afccc006ab8bb6c5953.chunk.js:ro
|
||||
- /opt/docker/jellyfin/web-overrides/empty-chunk.js:/jellyfin/jellyfin-web/fr-json.<hash>.chunk.js:ro
|
||||
... (×91 more)
|
||||
```
|
||||
|
||||
Where `empty-chunk.js` contents:
|
||||
```js
|
||||
(self.webpackChunk=self.webpackChunk||[]).push([[XXXXX],{}]);
|
||||
```
|
||||
|
||||
(XXXXX = chunk-id from runtime.bundle.js for that locale; chunk-ids
|
||||
listed in §"Files to Delete" below.)
|
||||
|
||||
Recommended: ship Path A first as the cheap belt; defer Path B to Phase 2
|
||||
unless the owner specifically wants the chunk files unreachable.
|
||||
|
||||
### Web agent — pre-auth splash fix (closes layer 18)
|
||||
|
||||
Append to the IIFE in `bin/inject-shim.py`, before the `start()` block:
|
||||
|
||||
```js
|
||||
// Override navigator.language BEFORE webpack bundles read it
|
||||
try {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
value: 'en-US', configurable: false, writable: false
|
||||
});
|
||||
Object.defineProperty(navigator, 'languages', {
|
||||
value: ['en-US', 'en'], configurable: false, writable: false
|
||||
});
|
||||
} catch(e){}
|
||||
```
|
||||
|
||||
Combined with Path A above (Accept-Language rewrite at proxy), pre-auth
|
||||
splash strings render in English on first paint.
|
||||
|
||||
### Docs agent — supersedes notes
|
||||
|
||||
After the above lands, update doc 15 with a "Status: applied 2026-05-XX"
|
||||
header and link forward to this doc. Update doc 16 F1 cross-ref since the
|
||||
manifest `name`/`short_name` work overlaps with the lockdown branch.
|
||||
|
||||
---
|
||||
|
||||
## Files to Delete (locale bundles served by web SPA)
|
||||
|
||||
> 92 non-English locale chunks served from `/jellyfin/jellyfin-web/`.
|
||||
> Hashes were captured from the live `runtime.bundle.js` chunk-id-to-hash
|
||||
> map on 2026-05-08; **these will rotate on every JF image upgrade** —
|
||||
> regenerate this list before each upgrade with:
|
||||
>
|
||||
> ```bash
|
||||
> curl -ks 'https://arrflix.s8n.ru/web/runtime.bundle.js?<query>' | python3 -c "
|
||||
> import re, sys
|
||||
> txt = sys.stdin.read()
|
||||
> hashmap = dict(re.findall(r'(\d+):\"([a-f0-9]{20})\"', txt))
|
||||
> namemap = dict(re.findall(r'(\d+):\"([a-zA-Z0-9_-]+-json)\"', txt))
|
||||
> for cid, name in sorted(namemap.items(), key=lambda x: x[1]):
|
||||
> if not name.startswith('en-us'):
|
||||
> print(f'{name}.{hashmap[cid]}.chunk.js')
|
||||
> "
|
||||
> ```
|
||||
|
||||
The single chunk to **keep** is `en-us-json.667484b4a441712c7e05.chunk.js`.
|
||||
|
||||
The 92 chunks to **drop** (current hashes — re-extract on upgrade):
|
||||
|
||||
```
|
||||
af-json.c51579ebcde4cc473828.chunk.js
|
||||
ar-json.1e4d5a6f9a6acf5777ba.chunk.js
|
||||
as-json.c9ec5dcf74b613f34865.chunk.js
|
||||
be-by-json.04e26c1f665c26cef640.chunk.js
|
||||
bg-bg-json.8f63ff103b1093a4367b.chunk.js
|
||||
bn-json.<hash>.chunk.js
|
||||
bn_BD-json.<hash>.chunk.js
|
||||
ca-json.<hash>.chunk.js
|
||||
ch-json.<hash>.chunk.js
|
||||
cs-json.<hash>.chunk.js
|
||||
cy-json.<hash>.chunk.js
|
||||
da-json.<hash>.chunk.js
|
||||
de-json.1afccc006ab8bb6c5953.chunk.js ← THE ONE THAT BIT US (contains "Play":"Abspielen")
|
||||
el-json.<hash>.chunk.js
|
||||
en-gb-json.<hash>.chunk.js ← keep? en-GB is also English; defer to owner. If owner wants only en-US, drop.
|
||||
eo-json.<hash>.chunk.js
|
||||
es-ar-json.<hash>.chunk.js
|
||||
es-json.<hash>.chunk.js
|
||||
es-mx-json.<hash>.chunk.js
|
||||
es_419-json.<hash>.chunk.js
|
||||
es_DO-json.<hash>.chunk.js
|
||||
et-json.<hash>.chunk.js
|
||||
eu-json.<hash>.chunk.js
|
||||
fa-json.<hash>.chunk.js
|
||||
fi-json.<hash>.chunk.js
|
||||
fil-json.<hash>.chunk.js
|
||||
fo-json.<hash>.chunk.js
|
||||
fr-ca-json.<hash>.chunk.js
|
||||
fr-json.<hash>.chunk.js
|
||||
ga-json.<hash>.chunk.js
|
||||
gl-json.<hash>.chunk.js
|
||||
gsw-json.<hash>.chunk.js
|
||||
gu-json.<hash>.chunk.js
|
||||
he-json.<hash>.chunk.js
|
||||
hi-in-json.<hash>.chunk.js
|
||||
hr-json.<hash>.chunk.js
|
||||
hu-json.<hash>.chunk.js
|
||||
hy-json.<hash>.chunk.js
|
||||
id-json.<hash>.chunk.js
|
||||
is-is-json.<hash>.chunk.js
|
||||
it-json.<hash>.chunk.js
|
||||
ja-json.<hash>.chunk.js
|
||||
jbo-json.<hash>.chunk.js
|
||||
ka-json.<hash>.chunk.js
|
||||
kab-json.<hash>.chunk.js
|
||||
kk-json.<hash>.chunk.js
|
||||
kn-json.<hash>.chunk.js
|
||||
ko-json.<hash>.chunk.js
|
||||
lt-lt-json.<hash>.chunk.js
|
||||
lv-json.<hash>.chunk.js
|
||||
mk-json.<hash>.chunk.js
|
||||
ml-json.<hash>.chunk.js
|
||||
mn-mn-json.<hash>.chunk.js
|
||||
mr-json.<hash>.chunk.js
|
||||
ms-json.<hash>.chunk.js
|
||||
nb-json.<hash>.chunk.js
|
||||
ne-json.<hash>.chunk.js
|
||||
nl-json.<hash>.chunk.js
|
||||
nn-json.<hash>.chunk.js
|
||||
pa-json.<hash>.chunk.js
|
||||
pl-json.<hash>.chunk.js
|
||||
pr-json.<hash>.chunk.js
|
||||
pt-br-json.<hash>.chunk.js
|
||||
pt-json.<hash>.chunk.js
|
||||
pt-pt-json.<hash>.chunk.js
|
||||
ro-json.<hash>.chunk.js
|
||||
ru-json.<hash>.chunk.js
|
||||
si-json.<hash>.chunk.js
|
||||
sk-json.<hash>.chunk.js
|
||||
sl-si-json.<hash>.chunk.js
|
||||
so-json.<hash>.chunk.js
|
||||
sq-json.<hash>.chunk.js
|
||||
sr-json.<hash>.chunk.js
|
||||
sv-json.<hash>.chunk.js
|
||||
sw-json.<hash>.chunk.js
|
||||
ta-json.<hash>.chunk.js
|
||||
te-json.<hash>.chunk.js
|
||||
th-json.<hash>.chunk.js
|
||||
tr-json.<hash>.chunk.js
|
||||
uk-json.<hash>.chunk.js
|
||||
ur_PK-json.<hash>.chunk.js
|
||||
uz-json.<hash>.chunk.js
|
||||
vi-json.5ce142c3b4228beafe7a.chunk.js
|
||||
zh-cn-json.9ef4c0ef42cc04d64912.chunk.js
|
||||
zh-hk-json.faa0648f6b0f186e6c07.chunk.js
|
||||
zh-tw-json.d07cd62eb7dd68687b64.chunk.js
|
||||
zu-json.0c869775f5145121570c.chunk.js
|
||||
... (full 92-line list saved to web-overrides/README.md when web agent regenerates)
|
||||
```
|
||||
|
||||
The full count is 93 chunk files served at runtime; one (`en-us-json.<hash>`)
|
||||
is kept, 92 are dropped. Decision required from owner: drop `en-gb-json`
|
||||
too, or accept en-GB as a tolerable secondary English locale? Doc 15 line 19
|
||||
mentioned `TARGET_LOCALE=en-GB` is an alternate option, suggesting en-GB is
|
||||
not categorically rejected. **Default recommendation: drop en-gb too —
|
||||
"English only, en-US canonical".**
|
||||
|
||||
---
|
||||
|
||||
## Cross-References
|
||||
|
||||
- `docs/15-force-english.md` — original per-user UICulture diagnosis +
|
||||
`bin/force-english-all-users.sh` (script exists, **not yet run**) +
|
||||
wrapper patch for `bin/add-jellyfin-user.sh` (**not yet applied**).
|
||||
This audit confirms doc 15's diagnosis is still accurate and adds the
|
||||
user-count update (5 → 8).
|
||||
- `docs/16-jellyfin-branding-leaks.md` — covers the Jellyfin word in PWA
|
||||
manifest `name`/`short_name` (F1), screensaver banner (F2), i18n keys
|
||||
containing "Jellyfin" in en-us-json chunk (F3). The PWA manifest `lang`
|
||||
field is already `en-US` so no action overlap; only the `name`/`short_name`
|
||||
work overlaps with this doc's branding-vs-locale axis. F3's DOM text
|
||||
rewrite shim is orthogonal — it strips the *word* Jellyfin from
|
||||
English strings, while this doc strips *non-English strings entirely*.
|
||||
- `docs/10-spa-runtime-shim.md` — vehicle for the proposed
|
||||
`Object.defineProperty(navigator, 'language', …)` snippet (see Layer 18).
|
||||
Same `inject-shim.py` already in use; one new `try/catch` block.
|
||||
- `docs/04-theming-and-users.md` — CustomCss is unrelated to locale; no
|
||||
overlap, no action.
|
||||
|
||||
---
|
||||
|
||||
## Sign-off
|
||||
|
||||
- **Audit run by:** s8n, 2026-05-08, admin token via `X-Emby-Token` header.
|
||||
- **Mode:** read-only. Zero POST/PATCH/PUT to Jellyfin. Zero file
|
||||
modifications outside this `docs/19-english-only-audit.md`.
|
||||
- **Live state:** all 8 users at UICulture-absent (root cause confirmed);
|
||||
93 locale bundles served (1 keep / 92 drop); SPA index.html serves
|
||||
byte-identical regardless of `Accept-Language` (locale is client-side);
|
||||
doc-15 fix exists but unrun; doc-15 wrapper patch unapplied.
|
||||
- **Recommended next action:** server agent runs `bin/force-english-all-users.sh`
|
||||
and applies the wrapper patch (closes 80% of the leak in 30 seconds).
|
||||
Web agent adds the Traefik `Accept-Language` middleware (Path A) and
|
||||
the `navigator.language` shim (closes the remaining pre-auth leak).
|
||||
Defer chunk bind-mounts (Path B) to Phase 2.
|
||||
Loading…
Reference in a new issue