- Domain: tv.s8n.ru retired (404). nasflix.s8n.ru live (302 → /web). Pi-hole local DNS updated. Traefik file-provider router rule + docker-label router rule both flipped. Jellyfin PublishedServerUrl env updated. Cert re-issued via Gandi DNS-01. Onyx /etc/hosts pin moved. - Repo: forgejo PATCH /api/v1/repos rename. Local clone remote URL updated. All in-tree refs to tv.s8n.ru and jellyfin-stack swept (sed). - Scope: TV Shows + Movies only. anime/, musicvideos/, home/, music/, docs-*/ libraries removed from canonical layout. Sections kept as reference for re-introduction. - Branding LoginDisclaimer text updated to nasflix.s8n.ru.
16 KiB
04 — Theming and Users
Status: applied 2026-05-08 against https://nasflix.s8n.ru (Jellyfin 10.10.3 on nullstone).
Scope: visual theme, server-side branding, multi-user UX prep, SyncPlay,
maintenance/revert. LAN-only constraints preserved (no public-facing changes).
1. Theme decision: ElegantFin
Candidates surveyed
| Theme | Type | Maintenance (May 2026) | Notes |
|---|---|---|---|
| Jellyfin built-in CSS variables | first-party | n/a | Minimal. Recolour only, no layout polish. |
| ElegantFin (lscambo13) | community CSS | active — v25.12.31 (Dec 2025), tested 10.11.5 | Jellyseerr-inspired. Single-import, jsDelivr-hosted. |
| Ultrachromic (CTalvio) | community CSS | "selectively maintained, project is old" | Three presets (mono / kaleido / nova). Risk of breaking on newer JF. |
| JellyFlix (prayag17) | community CSS | development halted per repo notice | Most Netflix-look-alike but stale. |
| DarkFlix (DevilsDesigns) | community CSS, fork of JellyFlix | sporadic | Inherits JellyFlix risk. |
| Theme Park (Jellyfin pack) | multi-app CSS | active | dracula/nord/hotline/plex variants. Less Netflix, more "skin pack". |
| Jellyfin-Vue | full alt web client | active | Replaces the entire web UI. Out of scope: violates "theme via CSS only" constraint and forces an image swap. |
| Finetwo / JellyfinNetflixWeb | fork-style replacement | varies | Same constraint violation. |
Why ElegantFin
- Most recent activity by far — v25.12.31 released 31 Dec 2025; tested on Jellyfin 10.11.5 which means it'll keep working as we upgrade past our current 10.10.3.
- Single
@importline — zero ops overhead. CDN-hosted on jsDelivr withcache-control: public, max-age=604800. No assets to host ourselves. Revert = clear one field. - Jellyseerr-inspired — modern dark UI with rounded cards, hero backdrops, smooth hover, condensed sidebar. Closer to "premium streaming" feel than Netflix's red-and-black, and ages better.
- Doesn't touch the upstream image — we stay on
jellyfin/jellyfin:10.10.3. - Compatible with multi-user setup — applies server-wide via
Branding.CustomCss, every user inherits. - Not the JellyWatch 2026 darling (that was JellyFlix) but JellyFlix is explicitly halted. ElegantFin is the safer, longer-lived pick.
What it looks like
- Inter typeface throughout (loaded from Google Fonts inside the CSS).
- Dark-only colour scheme (
color-scheme: dark), primary background#111827, secondary#1d2635(Tailwind slate territory). - Backdrop hero on item pages with darker bottom-gradient overlay.
- Rounded cards (~10px radius), subtle shadow, hover lift.
- "ElegantFin v25.12.31" tag in the footer (visible to users — fine for us).
- Login screen restyled into a centred card on a blurred backdrop.
2. How it was applied
Branding API (already done 2026-05-08)
TOKEN=*redacted*
cat > /tmp/branding.json <<'EOF'
{
"LoginDisclaimer": "Welcome to nasflix.s8n.ru — LAN-only. Be kind, rewind.",
"CustomCss": "/* ElegantFin v25.12.31 — Jellyseerr-inspired Netflix-y theme */\n@import url(\"https://cdn.jsdelivr.net/gh/lscambo13/ElegantFin@main/Theme/ElegantFin-jellyfin-theme-build-latest-minified.css\");\n",
"SplashscreenEnabled": true
}
EOF
curl -sS -X POST \
-H "X-Emby-Token: $TOKEN" \
-H "Content-Type: application/json" \
--data-binary @/tmp/branding.json \
https://nasflix.s8n.ru/System/Configuration/branding
# expect: HTTP 204
Verification
curl -sS -H "X-Emby-Token: $TOKEN" \
https://nasflix.s8n.ru/System/Configuration/branding | python3 -m json.tool
# Should include the @import line and the disclaimer text.
# Public branding endpoint the SPA reads at runtime — confirms anonymous
# clients (i.e. the browser before login) will see the theme:
curl -sS https://nasflix.s8n.ru/Branding/Configuration | python3 -m json.tool
/ returns Jellyfin's SPA shell; the theme CSS is fetched at runtime
by the JS bundle from /Branding/Configuration, not inlined into
index.html. So curl / won't grep-match. The valid JSON at
/Branding/Configuration is the API-level confirmation. Final check is a
hard browser reload (cache-bust) on https://nasflix.s8n.ru from the LAN.
Cache clear
Jellyfin web caches aggressively in browsers. After applying:
- Users: hard reload (Ctrl-Shift-R / Cmd-Shift-R) once.
- Server: no restart needed; CustomCss change is live on next page load.
3. Server-side branding state (as of 2026-05-08)
| Field | Value |
|---|---|
LoginDisclaimer |
"Welcome to nasflix.s8n.ru — LAN-only. Be kind, rewind." |
CustomCss |
@import of ElegantFin v25.12.31 from jsDelivr (autoupdating off @main) |
SplashscreenEnabled |
true |
SplashscreenEnabled: true makes Jellyfin auto-pick a backdrop from the
library and serve it at /Branding/Splashscreen. The web client doesn't
itself surface this; mobile/TV clients do. Harmless to leave on.
4. Multi-user UX prep
4a. Library inventory
| Library | Type | ItemId |
|---|---|---|
| Movies | movies | f137a2dd21bbc1b99aa5c0f6bf02a805 |
| TV Shows | tvshows | 767bffe4f11c93ef34b805451a696a4e |
| Playlists | playlists | 1071671e7bffa0532e930debee501d2e |
4b. Existing users
| Name | UserId | Admin? |
|---|---|---|
| s8n | 2be0f0d3fe3a45dc9298138a15a01925 |
yes |
4c. Creating a new user (UI)
- Dashboard → Users → "+ Add User".
- Username + initial password. Tick "User can manage server" OFF.
- After creation, click the user → tabs:
- Profile: language, audio default, subtitle default. Set per user; doesn't have to match server defaults.
- Library Access: untick "Enable access to all libraries", tick only the libraries this user should see.
- Parental Control: max rating, blocked tags, access schedule.
- Password: set / reset.
4d. Creating a new user (API) — playbook
Do not run this without explicit user request. Documented for the friend account that will exist later.
TOKEN=*redacted*
TVSHOWS_ID=767bffe4f11c93ef34b805451a696a4e
# 1. Create the user (auth header REQUIRED — admin token).
NEW_USER=$(curl -sS -X POST \
-H "X-Emby-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"Name":"friend","Password":"<initial-password>"}' \
https://nasflix.s8n.ru/Users/New)
echo "$NEW_USER" | python3 -m json.tool
NEW_ID=$(echo "$NEW_USER" | python3 -c "import sys,json; print(json.load(sys.stdin)['Id'])")
echo "NEW_ID=$NEW_ID"
# 2. Tighten the policy: TV-only, non-admin, can change own prefs,
# no content deletion, SyncPlay enabled (so we can co-watch).
cat > /tmp/policy.json <<EOF
{
"IsAdministrator": false,
"IsHidden": false,
"IsDisabled": false,
"EnableContentDeletion": false,
"EnableUserPreferenceAccess": true,
"EnableRemoteAccess": true,
"EnableSharedDeviceControl": false,
"EnableLiveTvAccess": false,
"EnableLiveTvManagement": false,
"EnableMediaPlayback": true,
"EnableAudioPlaybackTranscoding": true,
"EnableVideoPlaybackTranscoding": true,
"EnablePlaybackRemuxing": true,
"EnableAllFolders": false,
"EnabledFolders": ["$TVSHOWS_ID"],
"EnableAllChannels": false,
"EnabledChannels": [],
"EnableAllDevices": true,
"BlockedTags": [],
"BlockedMediaFolders": [],
"MaxParentalRating": null,
"AccessSchedules": [],
"SyncPlayAccess": "CreateAndJoinGroups",
"InvalidLoginAttemptCount": 0,
"LoginAttemptsBeforeLockout": 5,
"MaxActiveSessions": 0
}
EOF
curl -sS -X POST \
-H "X-Emby-Token: $TOKEN" \
-H "Content-Type: application/json" \
--data-binary @/tmp/policy.json \
"https://nasflix.s8n.ru/Users/$NEW_ID/Policy"
# expect: HTTP 204
# 3. Verify
curl -sS -H "X-Emby-Token: $TOKEN" \
"https://nasflix.s8n.ru/Users/$NEW_ID" | python3 -m json.tool
4e. Policy field cheat-sheet
| Field | What it does | Recommended for friend |
|---|---|---|
IsAdministrator |
Full server admin | false |
EnableContentDeletion |
Can delete media items via UI | false |
EnableUserPreferenceAccess |
Change own profile/audio/sub prefs | true |
EnableAllFolders |
Master switch for library ACL | false |
EnabledFolders |
Whitelist of CollectionFolder Ids | [TVShows] (only) |
BlockedTags |
Skip items tagged with these | optional; e.g. ["adult","unrated"] |
MaxParentalRating |
Hide above this rating | null for friend (adult). Set 15 for a kid. |
AccessSchedules |
Day-of-week + time windows | [] (no restriction) |
SyncPlayAccess |
CreateAndJoinGroups / JoinGroups / None |
CreateAndJoinGroups |
MaxActiveSessions |
Concurrent sessions cap; 0 = unlimited | 2 if you want to throttle |
LoginAttemptsBeforeLockout |
Brute-force protection | 5 |
EnableLiveTvAccess / Management |
Live TV / DVR | false (we don't run it) |
4f. Password reset flow
The friend forgot their password. Two routes:
- Self-serve (only if SMTP is configured — we don't currently): login
page → "Forgot Password". Jellyfin emits a PIN file at
/config/data/passwordreset-*.jsonvalid 30 minutes. Without SMTP, the admin reads the PIN out of the container and gives it to the friend. - Admin reset (what we'll do):
curl -sS -X POST \
-H "X-Emby-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"NewPw":"<new-password>","ResetPassword":false}' \
"https://nasflix.s8n.ru/Users/$USER_ID/Password"
To clear the password entirely (forces friend to set one on next login):
same call with "ResetPassword": true and no NewPw.
4g. Easy Pin / quick login
Jellyfin's built-in equivalent is Quick Connect:
- Dashboard → General → "Allow Quick Connect" (server-wide toggle).
- Friend opens a Jellyfin client (TV app, mobile), taps "Quick Connect" → 6-digit code.
- They enter the code in any already-logged-in browser session under Settings → Quick Connect → Authorize.
Casual-friendly and avoids them typing passwords on a TV remote. We have not enabled it yet — flip on when friend account is created.
4h. Per-user defaults (profile UI)
Set on each user's profile page (or via /Users/{id}/Configuration API):
AudioLanguagePreference:engSubtitleLanguagePreference:engSubtitleMode:Smart(only show when audio differs) orAlways.PlayDefaultAudioTrack: true.- Display language: pick on first login.
5. Watching together / continue-watching
5a. Resume / Next Up / Up Next — how Jellyfin builds them
- Continue Watching ("Resume" row): items where
UserData.PlaybackPositionTicks > 0and not yetPlayed: true. Threshold for "watched" is server-side ~90% by default. Per-user. - Next Up: for series the user has started, Jellyfin walks the next unwatched episode in season/episode order. Configurable in Dashboard → Display → Next Up (max age, rewatching toggle).
- Up Next (the in-player auto-advance card): client-side feature in the web/mobile players, fed by the same Next Up logic.
No action needed — these light up automatically once a user has played something. Futurama is loaded, so as soon as anyone plays an episode, the homepage gets populated.
5b. SyncPlay (synchronised group playback)
Server-side: nothing to enable, ships on. Per-user permission lives in
Policy.SyncPlayAccess:
| Value | Meaning |
|---|---|
CreateAndJoinGroups |
Can start a SyncPlay group + invite |
JoinGroups |
Can only join existing groups |
None |
Disabled |
Verified current state: s8n.SyncPlayAccess = CreateAndJoinGroups ✓.
How to use:
- s8n opens a series episode and starts playing.
- Player overlay → top-right people-icon ("SyncPlay") → "Create group".
- Friend logs in (any device — same
nasflix.s8n.ru), opens the same item or the SyncPlay menu → "Join {s8n}'s group". - Anyone in the group's play/pause/seek is mirrored within ~1 second.
- Voice chat is up to you — Jellyfin doesn't bundle one (Matrix room
on
txt.s8n.ruworks fine; or just a phone call).
Caveat: SyncPlay uses WebSockets. Our reverse proxy (traefik) handles
WS by default, no changes needed.
6. Maintenance
6a. Updating the theme
ElegantFin's @import URL pins to @main on jsDelivr — meaning new
upstream commits propagate after jsDelivr's cache TTL (12h s-maxage,
7d max-age). To pull immediately:
# Force refresh by pinning to a specific tag, then back to main:
curl -sS -X POST -H "X-Emby-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"CustomCss": "@import url(\"https://cdn.jsdelivr.net/gh/lscambo13/ElegantFin@v25.12.31/Theme/ElegantFin-jellyfin-theme-build-latest-minified.css\");", "LoginDisclaimer": "Welcome to nasflix.s8n.ru — LAN-only. Be kind, rewind.", "SplashscreenEnabled": true}' \
https://nasflix.s8n.ru/System/Configuration/branding
Or just ask each user to hard-reload — their browser cache is the common bottleneck, not jsDelivr.
When upgrading Jellyfin (e.g. 10.10.3 → 10.11.x), check the ElegantFin release notes first. The current theme is tagged tested-against 10.11.5, so we're forward-compatible through that.
6b. Reverting
Empty out the CustomCss field via API:
curl -sS -X POST -H "X-Emby-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"CustomCss": "", "LoginDisclaimer": "", "SplashscreenEnabled": false}' \
https://nasflix.s8n.ru/System/Configuration/branding
Or in the UI: Dashboard → General → clear "Custom CSS code" → Save. Hard-reload browsers. Vanilla Jellyfin returns instantly.
6c. Pinning a known-good revision
If @main ships a regression, switch the URL to a specific release tag
(e.g. @v25.12.31). Tags are in the GitHub releases page. jsDelivr
serves @<tag> immutably and forever.
7. First-30-minutes UX checklist (new user)
When the friend gets their account, walk them through this once:
- Login → see the LAN-only disclaimer; that's the right server.
- Profile picture → set one (just helps SyncPlay group UX).
- Display preferences (top-right user icon → Display):
- Theme: keep "Dark" (ElegantFin is dark-only, light theme will look half-applied). Don't switch.
- Landing screen: Home.
- Playback preferences:
- Default audio language:
English. - Default subtitle language:
English. - Subtitle mode:
Smart(auto-show on foreign audio). - "Play next episode automatically": on (this is what enables Up Next).
- Default audio language:
- Quality — first-time playback test on Futurama:
- Pick S01E01, play. Click the gear → quality. If it stutters on
1080p, drop to 720p; transcoder is CPU-only on nullstone today
(GTX 1660 Ti driver still broken — see
README.md). - Once confirmed playing, that quality is remembered per device.
- Pick S01E01, play. Click the gear → quality. If it stutters on
1080p, drop to 720p; transcoder is CPU-only on nullstone today
(GTX 1660 Ti driver still broken — see
- SyncPlay test: friend in one tab, s8n in another, friend joins s8n's group, confirm play/pause syncs. (Drops the "do you have it running" question forever.)
- Mobile/TV: install Jellyfin app, server URL
https://nasflix.s8n.ru(must be on LAN or Tailscale), Quick Connect or password. - Bookmarks/RSS: there isn't one — Jellyfin's "Latest" row is the substitute. Friend can favourite shows (heart icon) to pin.
8. Open items / future work
- Enable Quick Connect when friend account is created (Dashboard → General → Quick Connect).
- Configure SMTP for self-serve password reset (currently admin-only).
- Replace
@mainpin with@v25.12.31if we hit upstream churn. - Add a 2nd library (movies are mounted but the server may have an empty Movies folder — confirm with friend's first ask).
- After GPU driver fix on nullstone, NVENC transcode → 1080p HEVC will stop being CPU-bound; revisit per-user quality defaults.
- Optional: tweak
--elegantFinFooterTextCSS var to drop the ElegantFin version label from the footer (cosmetic).