middle-theme v6 + branding.xml video escape
Add ARRFLIX wordmark center, Movies/Series nav left, search right, favicon=A-mark, auth gate so login stays stock, hide on video page. Side-effect of branding.xml escape (<video> → <video>): prod's CustomCss block now actually loads, so the INC7 transparent-video rule reaches the browser. /Branding/Css.css 0 B → 36 256 B; doc-28 black-screen issue closed at the delivery layer. Markers: ARRFLIX-MIDDLE-THEME-BEGIN/END (style + script) and ARRFLIX-FAVICON-BEGIN/END (link). Idempotent. See docs/29 for design + deploy procedure + recovery quirk.
This commit is contained in:
parent
d0e7af3099
commit
52a7df6695
6 changed files with 394 additions and 1 deletions
140
bin/inject-middle-theme.py
Executable file
140
bin/inject-middle-theme.py
Executable file
|
|
@ -0,0 +1,140 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Inject the ARRFLIX middle-theme v6 (logo center, Movies/Series left, search right)
|
||||||
|
into a Jellyfin web overlay's index.html. Idempotent — run repeatedly without drift.
|
||||||
|
|
||||||
|
Markers:
|
||||||
|
/* ARRFLIX-MIDDLE-THEME-BEGIN */ ... /* ARRFLIX-MIDDLE-THEME-END */ inside <style> and <script>
|
||||||
|
<!--ARRFLIX-FAVICON-BEGIN--> ... <!--ARRFLIX-FAVICON-END--> between <link> tags
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 bin/inject-middle-theme.py [target.html]
|
||||||
|
ARRFLIX_OVERLAY_PATH=/opt/docker/jellyfin/web-overrides/index.html python3 bin/inject-middle-theme.py
|
||||||
|
|
||||||
|
Default target: <repo_root>/web-overrides/index.html
|
||||||
|
Assets read from <repo_root>/web-overrides/assets/:
|
||||||
|
- arrflix-A.b64 favicon base64 (no data: prefix)
|
||||||
|
- arrflix-wordmark.b64-url center-logo data-URL (with data: prefix)
|
||||||
|
|
||||||
|
Doc 29 covers the design, the auth gate, and the video-page hide rule.
|
||||||
|
"""
|
||||||
|
import os, re, sys, pathlib, time
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||||
|
DEFAULT_TARGET = ROOT / "web-overrides" / "index.html"
|
||||||
|
ASSETS = ROOT / "web-overrides" / "assets"
|
||||||
|
|
||||||
|
target = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else pathlib.Path(os.environ.get("ARRFLIX_OVERLAY_PATH", DEFAULT_TARGET))
|
||||||
|
if not target.exists():
|
||||||
|
sys.exit(f"target overlay not found: {target}")
|
||||||
|
|
||||||
|
logo_a_b64 = (ASSETS / "arrflix-A.b64").read_text(encoding="utf-8").strip()
|
||||||
|
wordmark_url = (ASSETS / "arrflix-wordmark.b64-url").read_text(encoding="utf-8").strip()
|
||||||
|
|
||||||
|
START = "/* ARRFLIX-MIDDLE-THEME-BEGIN */"
|
||||||
|
END = "/* ARRFLIX-MIDDLE-THEME-END */"
|
||||||
|
|
||||||
|
CSS = (
|
||||||
|
"body.arrflix-themed .skinHeader .headerTop{display:flex!important;align-items:center;position:relative;min-height:48px}\n"
|
||||||
|
"body.arrflix-themed .skinHeader .headerLeft,body.arrflix-themed .skinHeader .headerRight{flex:1 1 0;display:flex;align-items:center}\n"
|
||||||
|
"body.arrflix-themed .skinHeader .headerLeft{justify-content:flex-start;gap:.4em}\n"
|
||||||
|
"body.arrflix-themed .skinHeader .headerRight{justify-content:flex-end}\n"
|
||||||
|
"body.arrflix-themed .skinHeader .headerHomeButton,body.arrflix-themed .skinHeader .pageTitleWithLogo{display:none!important}\n"
|
||||||
|
"body.arrflix-themed .skinHeader .headerLeft > h3.pageTitle:not(.pageTitleWithLogo){display:none!important}\n"
|
||||||
|
"body.arrflix-themed .skinHeader .headerCastButton,body.arrflix-themed .skinHeader .headerSyncButton{display:none!important}\n"
|
||||||
|
"body.arrflix-themed .headerTabs.sectionTabs{display:none!important}\n"
|
||||||
|
"/* Hide entire header during video playback */\n"
|
||||||
|
"body.arrflix-video-active:not(:has(#loginPage:not(.hide))) .skinHeader,body.arrflix-video-active .arrflix-headerLogo,body.arrflix-video-active .arrflix-nav{display:none!important}\n"
|
||||||
|
".arrflix-headerLogo{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:120px;height:38px;"
|
||||||
|
"background:center/contain no-repeat url('" + wordmark_url + "');"
|
||||||
|
"z-index:1;display:block;text-indent:-9999px;overflow:hidden}\n"
|
||||||
|
".arrflix-headerLogo:hover{filter:brightness(1.15)}\n"
|
||||||
|
".arrflix-nav{text-transform:uppercase;letter-spacing:.08em;font-weight:600;padding:0 .9em;color:#fff!important;text-decoration:none;display:inline-flex;align-items:center;height:100%;font-size:.85em}\n"
|
||||||
|
".arrflix-nav:hover{color:#E50914!important}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
JS = """
|
||||||
|
(function(){
|
||||||
|
function isVideoPage(){
|
||||||
|
try{
|
||||||
|
var h=(location.hash||'').toLowerCase();
|
||||||
|
if (h.indexOf('/video') !== -1) return true;
|
||||||
|
var osd = document.querySelector('#videoOsdPage:not(.hide)');
|
||||||
|
if (osd) return true;
|
||||||
|
var v = document.querySelector('.htmlVideoPlayer:not(.hide), video.htmlvideoplayer:not(.hide)');
|
||||||
|
if (v && getComputedStyle(v).display !== 'none') return true;
|
||||||
|
}catch(e){}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function isAuthed(){
|
||||||
|
try{
|
||||||
|
if (document.querySelector('.pageContainer.loginPage:not(.hide)')) return false;
|
||||||
|
if (document.querySelector('#loginPage:not(.hide)')) return false;
|
||||||
|
var h = (location.hash || '').toLowerCase();
|
||||||
|
if (h.indexOf('/login') !== -1 || h.indexOf('/wizard') !== -1 || h.indexOf('/forgotpassword') !== -1 || h.indexOf('/selectserver') !== -1) return false;
|
||||||
|
if (window.ApiClient && typeof window.ApiClient.isLoggedIn === 'function' && !window.ApiClient.isLoggedIn()) return false;
|
||||||
|
var raw = localStorage.getItem('jellyfin_credentials');
|
||||||
|
if (!raw) return false;
|
||||||
|
var creds = JSON.parse(raw);
|
||||||
|
if (!creds || !creds.Servers || !creds.Servers.length || !creds.Servers[0].AccessToken) return false;
|
||||||
|
return true;
|
||||||
|
} catch(e){ return false; }
|
||||||
|
}
|
||||||
|
function teardown(){
|
||||||
|
document.body.classList.remove('arrflix-themed');
|
||||||
|
var top = document.querySelector('.skinHeader .headerTop'); if (!top) return;
|
||||||
|
var logo = top.querySelector('.arrflix-headerLogo'); if (logo) logo.remove();
|
||||||
|
Array.prototype.forEach.call(document.querySelectorAll('.arrflix-nav'), function(n){ n.remove(); });
|
||||||
|
}
|
||||||
|
function relayoutHeader(){
|
||||||
|
document.body.classList.toggle('arrflix-video-active', isVideoPage());
|
||||||
|
if (!isAuthed()) { teardown(); return; }
|
||||||
|
var top=document.querySelector('.skinHeader .headerTop'); if(!top) return;
|
||||||
|
document.body.classList.add('arrflix-themed');
|
||||||
|
var left=top.querySelector('.headerLeft');
|
||||||
|
if(left && !left.querySelector('[data-arrflix-nav=\"movies\"]')){
|
||||||
|
left.insertAdjacentHTML('beforeend',
|
||||||
|
'<a is=\"emby-linkbutton\" class=\"emby-button arrflix-nav\" data-arrflix-nav=\"movies\" href=\"#/movies.html\">Movies</a>'+
|
||||||
|
'<a is=\"emby-linkbutton\" class=\"emby-button arrflix-nav\" data-arrflix-nav=\"series\" href=\"#/tv.html\">Series</a>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if(!top.querySelector('.arrflix-headerLogo')){
|
||||||
|
var a=document.createElement('a');
|
||||||
|
a.className='arrflix-headerLogo';
|
||||||
|
a.href='#/home.html';
|
||||||
|
a.setAttribute('aria-label','ARRFLIX home');
|
||||||
|
a.textContent='ARRFLIX';
|
||||||
|
var right=top.querySelector('.headerRight');
|
||||||
|
top.insertBefore(a, right || null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function start(){
|
||||||
|
relayoutHeader();
|
||||||
|
try{ new MutationObserver(relayoutHeader).observe(document.body,{childList:true,subtree:true}); }catch(e){}
|
||||||
|
window.addEventListener('hashchange', relayoutHeader);
|
||||||
|
setInterval(relayoutHeader,1500);
|
||||||
|
}
|
||||||
|
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',start,{once:true}); else start();
|
||||||
|
})();
|
||||||
|
"""
|
||||||
|
|
||||||
|
FAVICON_LINKS = (
|
||||||
|
"<!--ARRFLIX-FAVICON-BEGIN-->"
|
||||||
|
"<link rel=\"icon\" type=\"image/png\" sizes=\"180x180\" href=\"data:image/png;base64," + logo_a_b64 + "\">"
|
||||||
|
"<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"data:image/png;base64," + logo_a_b64 + "\">"
|
||||||
|
"<!--ARRFLIX-FAVICON-END-->"
|
||||||
|
)
|
||||||
|
|
||||||
|
src = target.read_text(encoding="utf-8")
|
||||||
|
src = re.sub(re.escape("<style>" + START) + r".*?" + re.escape(END + "</style>"), "", src, flags=re.DOTALL)
|
||||||
|
src = re.sub(re.escape("<script>" + START) + r".*?" + re.escape(END + "</script>"), "", src, flags=re.DOTALL)
|
||||||
|
src = re.sub(r"<!--ARRFLIX-FAVICON-BEGIN-->.*?<!--ARRFLIX-FAVICON-END-->", "", src, flags=re.DOTALL)
|
||||||
|
|
||||||
|
PATCH = "<style>" + START + CSS + END + "</style>" + "<script>" + START + JS + END + "</script>" + FAVICON_LINKS
|
||||||
|
if "</head>" not in src:
|
||||||
|
sys.exit("no </head> in target")
|
||||||
|
src2 = src.replace("</head>", PATCH + "</head>", 1)
|
||||||
|
|
||||||
|
backup = target.with_suffix(target.suffix + f".bak.pre-middle-v6.{int(time.time())}")
|
||||||
|
backup.write_text(target.read_text(encoding="utf-8"), encoding="utf-8")
|
||||||
|
target.write_text(src2, encoding="utf-8")
|
||||||
|
print(f"OK v6 wrote {len(src2)} bytes to {target}; backup at {backup}")
|
||||||
174
docs/29-middle-theme-v6-2026-05-09.md
Normal file
174
docs/29-middle-theme-v6-2026-05-09.md
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
# 29 — Middle-Theme v6 + Prod Stream Restore (2026-05-09)
|
||||||
|
|
||||||
|
> Outcome: ARRFLIX wordmark logo dead-center, Movies/Series nav left, search right; auth-gated so login page is untouched; header hidden during video playback. Same patch shipped to prod simultaneously with the **branding.xml `<video>` XML escape** that restored the INC7 transparent-video CSS — closing the live black-screen issue users saw on prod.
|
||||||
|
|
||||||
|
Status: **DEPLOYED 2026-05-09** — dev (`dev.arrflix.s8n.ru`) and prod (`arrflix.s8n.ru`) both serve `web-overrides/index.html` md5 `c6c85076951633c434864a0133d602e5`. Prod `/Branding/Css.css` went 0 B → 36 256 B post-fix.
|
||||||
|
|
||||||
|
Sibling docs: 28 (prod-vs-dev playback divergence — INC7 streaming fix), 26 (incident chain INC1–INC5), 12 (dev mirror), 17 (dev mirror + settings fix).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What v6 ships
|
||||||
|
|
||||||
|
1. **ARRFLIX wordmark, dead-center** in `.skinHeader .headerTop`. `.arrflix-headerLogo` is an `<a href="#/home.html">` with `position:absolute; left:50%; transform:translate(-50%,-50%)`. Background-image inlined as base64 (the same wordmark already used by `.adminDrawerLogo img` and `.pageTitleWithLogo` in `branding.xml`'s `CustomCss`). Width 120, height 38, aspect 235:85.
|
||||||
|
2. **Movies + Series uppercase nav links** injected into `.headerLeft`. `<a is="emby-linkbutton" class="emby-button arrflix-nav" href="#/movies.html">Movies</a>` (and `#/tv.html` for Series). The link `href` is bare — no `topParentId` query — so Jellyfin's `MoviesPage` resolves the library via user policy.
|
||||||
|
3. **Search button on the right** — Jellyfin's stock `.headerSearchButton` left untouched. `.headerLeft, .headerRight { flex:1 1 0 }` + `.headerRight { justify-content: flex-end }` push it to the corner.
|
||||||
|
4. **Stock clutter hidden** under `body.arrflix-themed`: `.headerHomeButton`, `.pageTitleWithLogo`, `.headerCastButton`, `.headerSyncButton`, `.headerTabs.sectionTabs`, and the bare `h3.pageTitle:not(.pageTitleWithLogo)` (the duplicate "Movies" title that appeared on library pages).
|
||||||
|
5. **Favicon swap** to the ARRFLIX "A" mark — injected as `<link rel="icon" type="image/png" href="data:image/png;base64,…">` plus `apple-touch-icon`, both wrapped in `<!--ARRFLIX-FAVICON-BEGIN/END-->` markers for idempotent re-runs.
|
||||||
|
6. **Auth gate.** `body.arrflix-themed` is added by JS only when `ApiClient.isLoggedIn()` AND `localStorage.jellyfin_credentials` has an `AccessToken` AND the current `location.hash` is not on `/login|/wizard|/forgotpassword|/selectserver`. CSS rules are scoped to `body.arrflix-themed` so the login page renders stock-with-Cineplex (ARRFLIX top-left red, Manual Login form) — not the rearranged middle-theme.
|
||||||
|
7. **Video page suppression.** When `location.hash` includes `/video` OR `#videoOsdPage:not(.hide)` is in the DOM OR a visible `.htmlVideoPlayer` exists, JS adds `body.arrflix-video-active`. CSS rule `body.arrflix-video-active:not(:has(#loginPage:not(.hide))) .skinHeader, body.arrflix-video-active .arrflix-headerLogo, body.arrflix-video-active .arrflix-nav { display:none !important }` — specificity (0,4,2) beats Cineplex's `body:not(:has(#loginPage:not(.hide))) .skinHeader { display:flex !important }` (0,3,2), so our hide wins.
|
||||||
|
|
||||||
|
JS uses `MutationObserver` on `body` + `hashchange` listener + `setInterval(1500)` watchdog. Idempotent: re-entry checks via `[data-arrflix-nav="movies"]` selector.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
The patch is a single Python script: `bin/inject-middle-theme.py`. It:
|
||||||
|
|
||||||
|
1. Reads the target HTML (default `/opt/docker/jellyfin/web-overrides/index.html` — overridable via env var `ARRFLIX_OVERLAY_PATH`).
|
||||||
|
2. Strips any prior `<style>ARRFLIX-MIDDLE-THEME-BEGIN…END</style>`, `<script>…BEGIN…END</script>`, and `<!--ARRFLIX-FAVICON-BEGIN→END-->` blocks (idempotent — safe to re-run).
|
||||||
|
3. Reads two artifacts:
|
||||||
|
- `web-overrides/assets/arrflix-A.png` (encoded inline as base64 for favicon)
|
||||||
|
- The wordmark base64 embedded in `branding.xml` (extracted at build time)
|
||||||
|
4. Inlines a `<style>` block, a `<script>` block, and two `<link>` tags into `<head>` immediately before `</head>`.
|
||||||
|
5. Writes a backup at `<target>.bak.pre-middle-v6.<timestamp>` before overwriting.
|
||||||
|
|
||||||
|
Re-run safely — old marker blocks are stripped first; result is byte-deterministic (same inputs → same md5).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stream-restore side-fix
|
||||||
|
|
||||||
|
Prod's `branding.xml` had a `<video>` literal in a CSS comment (BLACK-PASS section explaining INC7's transparent-video rule). The XML parser choked on the unescaped `<` → Jellyfin silently dropped the entire `<CustomCss>` block → the INC7 transparent-video rule never reached the browser → `#videoOsdPage` rendered an opaque black `.libraryPage` background OVER the decoded `<video>` frames → users saw black screens during playback.
|
||||||
|
|
||||||
|
`/Branding/Css.css` returned **0 bytes** until this was fixed (and **36 256 bytes** after).
|
||||||
|
|
||||||
|
Fix: escape the two unescaped `<video>` tokens to `<video>`. Before:
|
||||||
|
|
||||||
|
```
|
||||||
|
on top of <video> as opaque black -> visually black despite <video>
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```
|
||||||
|
on top of <video> as opaque black -> visually black despite <video>
|
||||||
|
```
|
||||||
|
|
||||||
|
XML now passes `xmllint --noout` cleanly. Same fix applied to dev simultaneously — both branding.xml files now have md5 `<see config>` and parse identically.
|
||||||
|
|
||||||
|
This single character-level escape is what restored streaming on prod. The doc-28 chain (Traefik SW pin, INC7 transparent CSS) was technically correct upstream — the diagnosis was right, but the *delivery* was broken because the XML never loaded. INC7's CSS rule had been "in" `branding.xml` since 2026-05-09 02:46Z, but `Branding/Css.css` was empty so the rule never reached any browser.
|
||||||
|
|
||||||
|
**Lesson:** add `xmllint --noout branding.xml` to deploy CI. The user-visible failure mode of a malformed `BrandingOptions` XML is silent (zero-byte response, no banner, no admin notification), and both prod and dev had been running unthemed-via-CustomCss for multiple deploy cycles before anyone noticed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files touched
|
||||||
|
|
||||||
|
| Path | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `web-overrides/index.html` | Apply `bin/inject-middle-theme.py` — adds 75 KB (wordmark + favicon base64 + style + script + link). Idempotent markers `ARRFLIX-MIDDLE-THEME-BEGIN/END` and `ARRFLIX-FAVICON-BEGIN/END`. md5 `c6c85076951633c434864a0133d602e5`. |
|
||||||
|
| `web-overrides/assets/arrflix-A.png` | New — 1695×928 PNG of the ARRFLIX "A" mark on white. Source for the favicon (white→transparent + resize to 138×180 → base64 inline). |
|
||||||
|
| `bin/inject-middle-theme.py` | New — the patch builder. |
|
||||||
|
| `docs/29-middle-theme-v6-2026-05-09.md` | This doc. |
|
||||||
|
| **Server-side** `/home/docker/jellyfin/config/config/branding.xml` (prod) | Two `<video>` → `<video>` escapes. **Not in repo** (config is per-deployment; document the change here). |
|
||||||
|
| **Server-side** `/home/docker/jellyfin-dev/config/config/branding.xml` (dev) | Same escape. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deploy procedure
|
||||||
|
|
||||||
|
### Dev
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Re-run patch builder against dev's overlay (idempotent)
|
||||||
|
python3 bin/inject-middle-theme.py
|
||||||
|
scp web-overrides/index.html user@192.168.0.100:/opt/docker/jellyfin-dev/web-overrides/index-dev.html
|
||||||
|
# Single-file bind mount — no container restart needed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prod
|
||||||
|
|
||||||
|
Prod's overlay file is owned `root:root`, so `ssh user@…` can't write directly. Use a docker-as-root shim:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --userns=host \
|
||||||
|
-v /opt/docker/jellyfin/web-overrides:/d:rw \
|
||||||
|
-v /tmp:/tmp:rw \
|
||||||
|
alpine sh -c '
|
||||||
|
apk add --no-cache python3 >/dev/null 2>&1 &&
|
||||||
|
python3 /tmp/inject-middle-theme.py /d/index.html
|
||||||
|
'
|
||||||
|
docker run --rm --userns=host -v /opt/docker/jellyfin/web-overrides:/d:rw \
|
||||||
|
alpine chown root:root /d/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
If `branding.xml` was rewritten with new content, also escape any new `<video>` (or any other unescaped `<`) and `xmllint --noout` before restart. Then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker restart jellyfin
|
||||||
|
# 30s downtime; users will need to refresh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec jellyfin curl -s http://127.0.0.1:8096/Branding/Css.css | wc -c # expect ~36 KB
|
||||||
|
docker exec jellyfin curl -s http://127.0.0.1:8096/web/index.html | grep -c ARRFLIX-MIDDLE-THEME-BEGIN # expect 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Headless visual: run `bin/headless-test-v2.py` against prod with a known user — `darkPct` on the OSD frame should drop from ~100 % (pre-fix) to <10 % (post-fix), per the doc-28 INC7-final lesson.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Account state on dev
|
||||||
|
|
||||||
|
Dev jellyfin instance currently hosts a **single account** for theme testing:
|
||||||
|
|
||||||
|
| User | Password | Admin | Hidden |
|
||||||
|
|------|----------|-------|--------|
|
||||||
|
| `test` | `123` | yes | no |
|
||||||
|
|
||||||
|
The 7 mirror accounts (`marco-mirror`, `house-mirror`, `guest-mirror`, `aloy-mirror`, `pet-mirror`, `5-mirror`, `s8n-dev`) were deleted earlier in the session per owner's "replace all" decision. Library content (Movies + TV Shows) was inherited from prod via a one-time `/config` rsync (excluded `data/jellyfin.db`) so dev sees the same titles and metadata as prod.
|
||||||
|
|
||||||
|
**Recovery quirk:** `test`'s password gets nuked occasionally after `docker cp jellyfin.db` operations because `userns_mode: host` flips ownership back to host uid 101000 (the userns-remap of container 1000). Recovery cycle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker stop jellyfin-dev
|
||||||
|
docker cp jellyfin-dev:/config/data/jellyfin.db /tmp/r.db
|
||||||
|
docker cp jellyfin-dev:/config/data/jellyfin.db-wal /tmp/r.db-wal 2>/dev/null
|
||||||
|
sqlite3 /tmp/r.db 'PRAGMA wal_checkpoint(TRUNCATE); UPDATE Users SET Password=NULL, InvalidLoginAttemptCount=0 WHERE Username="test";'
|
||||||
|
docker cp /tmp/r.db jellyfin-dev:/config/data/jellyfin.db
|
||||||
|
docker exec --user 0 jellyfin-dev sh -c 'rm -f /config/data/jellyfin.db-wal /config/data/jellyfin.db-shm; chown 1000:1000 /config/data/jellyfin.db'
|
||||||
|
docker restart jellyfin-dev && sleep 9
|
||||||
|
# Authenticate with blank password, then POST /Users/{id}/Password { "CurrentPw":"", "NewPw":"123" }
|
||||||
|
```
|
||||||
|
|
||||||
|
User ID for `test`: `a0ea2751d4e2467cb634485614a959e8`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open follow-ups
|
||||||
|
|
||||||
|
| Item | Where |
|
||||||
|
|------|-------|
|
||||||
|
| `compose-dev/docker-compose.yml` in repo lacks the overlay bind-mount that the live host has | `compose-dev/docker-compose.yml` |
|
||||||
|
| Dev's `system.xml` has `QuickConnectAvailable=true`, prod has `false` — Quick Connect button visible on dev login only | `system.xml` line ~7 |
|
||||||
|
| Locale-en-only chunk JS files (`*-json.*.chunk.js`) bind-mounted on prod (94 of them) but absent on dev → dev users get stock locale strings | host `/opt/docker/jellyfin/web-overrides/locale-en-only/` |
|
||||||
|
| Movies/Shows pages on dev show a stuck spinner because Jellyfin's `tryRestoreView` bounces a cached `?topParentId=movies` URL → `/Items/movies` 400. Not a v6 regression — present in stock build too. | Jellyfin `viewContainer.tryRestoreView` |
|
||||||
|
| Add `xmllint --noout branding.xml` to repo CI | new |
|
||||||
|
| Headless `darkPct` assertion to surface CSS-overlay-over-video regressions automatically | `bin/headless-test-v2.py` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Snapshot
|
||||||
|
|
||||||
|
| Asset | md5 |
|
||||||
|
|-------|-----|
|
||||||
|
| `web-overrides/index.html` (post-v6) | `c6c85076951633c434864a0133d602e5` |
|
||||||
|
| `branding.xml` (prod, post-escape) | (see live config) |
|
||||||
|
| `branding.xml` (dev, post-escape) | (see live config) |
|
||||||
|
| `arrflix-A.png` (asset source) | (see repo) |
|
||||||
|
|
||||||
|
Both deploy targets running `c6c85076951633c434864a0133d602e5` as of 2026-05-09 ~03:00 UTC.
|
||||||
1
web-overrides/assets/arrflix-A.b64
Normal file
1
web-overrides/assets/arrflix-A.b64
Normal file
File diff suppressed because one or more lines are too long
BIN
web-overrides/assets/arrflix-A.png
Normal file
BIN
web-overrides/assets/arrflix-A.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 517 KiB |
1
web-overrides/assets/arrflix-wordmark.b64-url
Normal file
1
web-overrides/assets/arrflix-wordmark.b64-url
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue