.claude/skills/jellyfin/ was a real directory holding a stale fork of the canonical server-management copy — not the symlink CLAUDE.md describes. It presented X-Emby-Token as the auth pattern (v12 only accepts it via EnableLegacyAuthorization, slated for removal in v13) and omitted the DELETE /Items/{id} data-loss warning that has destroyed files three times.
Correction to the issue's premise
The issue comment framed server-management's ersatztv copy as "stale and dangerous" and ersatztv's as "correct". That is half right, and acting on it literally would have lost content:
server-management's copy is wrong only in its description: frontmatter (the "CRUD requires SQLite DB scripting" claim). Its body already said "Full REST CRUD is available under /api/v1; prefer it over direct DB writes".
Its body also carries ~150 lines the ersatztv copy lacks: backup/deploy scripts (#482), test/prod topology (#481), the streaming-mode + Dispatcharr teardown-race analysis (#500), VAAPI-vs-QSV on jazz (#498), table schema notes, and the channel-creation checklist.
So the reconciliation takes server-management's file as the base and ports ersatztv's unique content onto it — not the other way round.
Changes
jellyfin — directory deleted, replaced with a relative symlink to ~/server-management/.claude/skills/jellyfin, matching the pattern server-management already uses for beets/radarr/sonarr/yt-dlp.
ersatztv — reconciled as the canonical copy (ersatztv owns it per CLAUDE.md → Project Boundaries):
fixed the dangerous description: — it now says the REST API has write paths and SQLite is a fallback
ported ersatztv-only content: the silent root-owned api.key 401 trap, the lying OCI labels (#350), the local-vs-UTC log timestamp trap, the docs/endpoint-index.md pointer, settings/ffmpeg ordering, the scoped test-image refresh
added the #616 paging facts requested in the issue comment: 0-based pageNum, per-endpoint pageSize cap, and that playout/reset takes a channel id
Stale claims corrected in both copies (found while reconciling, verified against the decisions catalog):
prod auto-update: auto_update: false, manual DeployStack jazz-media only, no 03:00 fallback — the old text contradicted the fork's own CLAUDE.md
ErsatzTV library ids: Music Videos is 16 and Standup 14, not 8/9 — two of four entries were wrong; verified live via GET /api/v1/media-sources. Also noted these are ErsatzTV ids, not Jellyfin's
POST /api/v1/libraries/{id}/scan is the scan endpoint (there is no PUT …/sync)
external-URL logos do drive the on-screen bug now — the WatermarkSelectorFile.Exists() gate was removed in #502 (ffmpeg.external-logo-graphics-engine), logos cached at save time (graphics.channel-logo-caching)
M3U/XMLTV absolute URLs are no longer stuck on the request host — #340 added iptv.base_url (iptv.base-url)
unpinned the stale v26.8.0 image reference (current release is v26.12.0)
CLAUDE.md — both boundary lines now describe the actual arrangement, including the worktree caveat below.
Known caveat
A relative cross-repo symlink resolves only in a checkout at ~/ersatztv, so the jellyfin skill is unavailable inside a git worktree. This is inherent to the pattern server-management already uses; documented in CLAUDE.md rather than diverging to absolute paths (deviation policy).
Follow-up
The server-management side (~/server-management/.claude/skills/ersatztv → symlink into this repo) lands as a separate commit in that repo after this merges, so the symlink target already holds the reconciled content.
Verification
Symlink resolves from ~/ersatztv to the canonical jellyfin SKILL.md
Every stale claim above re-checked against docs/decisions/README.md records or live prod API, not memory
Docs-only / skills-only: no C# changed, so the independent-review rubric's code triggers (locks, auth handlers, migrations, >150 C# lines) don't apply. Touching .claude/ means the review-verdict auto-pass does not apply, so a real verdict is required.
fixes #617
## What was wrong
`.claude/skills/jellyfin/` was a real directory holding a stale fork of the canonical server-management copy — not the symlink `CLAUDE.md` describes. It presented `X-Emby-Token` as *the* auth pattern (v12 only accepts it via `EnableLegacyAuthorization`, slated for removal in v13) and omitted the `DELETE /Items/{id}` data-loss warning that has destroyed files three times.
## Correction to the issue's premise
The issue comment framed server-management's `ersatztv` copy as "stale and dangerous" and ersatztv's as "correct". That is **half right**, and acting on it literally would have lost content:
- server-management's copy is wrong **only in its `description:` frontmatter** (the "CRUD requires SQLite DB scripting" claim). Its **body** already said *"Full REST CRUD is available under `/api/v1`; prefer it over direct DB writes"*.
- Its body also carries **~150 lines the ersatztv copy lacks**: backup/deploy scripts (#482), test/prod topology (#481), the streaming-mode + Dispatcharr teardown-race analysis (#500), VAAPI-vs-QSV on jazz (#498), table schema notes, and the channel-creation checklist.
So the reconciliation takes **server-management's file as the base** and ports ersatztv's unique content onto it — not the other way round.
## Changes
**`jellyfin`** — directory deleted, replaced with a relative symlink to `~/server-management/.claude/skills/jellyfin`, matching the pattern server-management already uses for `beets`/`radarr`/`sonarr`/`yt-dlp`.
**`ersatztv`** — reconciled as the canonical copy (ersatztv owns it per `CLAUDE.md` → Project Boundaries):
- fixed the dangerous `description:` — it now says the REST API has write paths and SQLite is a fallback
- ported ersatztv-only content: the **silent root-owned `api.key` 401 trap**, the **lying OCI labels** (#350), the **local-vs-UTC log timestamp** trap, the `docs/endpoint-index.md` pointer, `settings/ffmpeg` ordering, the scoped test-image refresh
- added the **#616 paging facts** requested in the issue comment: 0-based `pageNum`, per-endpoint `pageSize` cap, and that `playout/reset` takes a **channel** id
**Stale claims corrected in both copies** (found while reconciling, verified against the decisions catalog):
- prod auto-update: `auto_update: false`, manual `DeployStack jazz-media` only, **no 03:00 fallback** — the old text contradicted the fork's own `CLAUDE.md`
- **ErsatzTV library ids: Music Videos is 16 and Standup 14**, not 8/9 — two of four entries were wrong; verified live via `GET /api/v1/media-sources`. Also noted these are ErsatzTV ids, not Jellyfin's
- `POST /api/v1/libraries/{id}/scan` is the scan endpoint (there is no `PUT …/sync`)
- external-URL logos **do** drive the on-screen bug now — the `WatermarkSelector` `File.Exists()` gate was removed in #502 (`ffmpeg.external-logo-graphics-engine`), logos cached at save time (`graphics.channel-logo-caching`)
- M3U/XMLTV absolute URLs are no longer stuck on the request host — #340 added `iptv.base_url` (`iptv.base-url`)
- unpinned the stale `v26.8.0` image reference (current release is v26.12.0)
**`CLAUDE.md`** — both boundary lines now describe the actual arrangement, including the worktree caveat below.
## Known caveat
A **relative** cross-repo symlink resolves only in a checkout at `~/ersatztv`, so the `jellyfin` skill is unavailable inside a git worktree. This is inherent to the pattern server-management already uses; documented in `CLAUDE.md` rather than diverging to absolute paths (deviation policy).
## Follow-up
The server-management side (`~/server-management/.claude/skills/ersatztv` → symlink into this repo) lands as a separate commit in that repo **after** this merges, so the symlink target already holds the reconciled content.
## Verification
- Symlink resolves from `~/ersatztv` to the canonical jellyfin `SKILL.md`
- Every stale claim above re-checked against `docs/decisions/README.md` records or live prod API, not memory
- Docs-only / skills-only: **no C# changed**, so the independent-review rubric's code triggers (locks, auth handlers, migrations, >150 C# lines) don't apply. Touching `.claude/` means the review-verdict auto-pass does **not** apply, so a real verdict is required.
`.claude/skills/jellyfin/` was a real directory holding a stale fork of the
canonical server-management copy, not the symlink CLAUDE.md described. It
documented `X-Emby-Token` as *the* auth pattern (v12 only accepts it via
`EnableLegacyAuthorization`, slated for removal in v13) and omitted the
`DELETE /Items/{id}` data-loss warning that has destroyed files three times.
Replaced with a relative symlink, matching the pattern server-management
already uses for beets/radarr/sonarr.
`.claude/skills/ersatztv/` had diverged in BOTH directions, so a replace in
either direction would have lost correct content. Contrary to the issue's
framing, server-management's copy was stale only in its `description:` — its
body already said "Full REST CRUD is available under /api/v1" and carried
~150 lines the ersatztv copy lacked (backup/deploy, test/prod topology,
streaming-mode analysis, VAAPI-vs-QSV, table schema notes, checklists). So
that file is the base, reconciled here as the canonical copy:
- fixed the dangerous `description:` claiming CRUD requires SQLite scripting
- ported ersatztv-only content: the silent root-owned api.key 401 trap, the
lying OCI labels, the local-vs-UTC log timestamp trap, the endpoint-index
pointer, `settings/ffmpeg` ordering, scoped test-image refresh
- added the #616 paging facts (0-based pageNum, per-endpoint pageSize cap,
playout/reset takes a CHANNEL id) per the request on #617
- corrected prod auto-update: `auto_update: false`, manual DeployStack only,
no 03:00 fallback (contradicted the fork's CLAUDE.md)
- corrected the ErsatzTV library ids: Music Videos is 16 and Standup 14, not
8/9 — verified live via GET /api/v1/media-sources
- unpinned the stale v26.8.0 image reference (current release is v26.12.0)
fixes#617
Both copies still described pre-fix behavior:
- "External URL logos work for M3U but NOT for watermark burn-in (code
checks File.Exists())" — the WatermarkSelector File.Exists gate was
removed in #502 (ffmpeg.external-logo-graphics-engine); logos are cached
at save time (graphics.channel-logo-caching).
- "M3U tvg-logo URLs hardcode http://localhost:8409" — #340 added the
optional iptv.base_url advertised base URL (iptv.base-url).
The reconciled description ends with 'IMPORTANT: the fork has a full
versioned REST API...'. An unquoted YAML scalar containing ': ' parses as
a mapping, so the frontmatter failed to load ('mapping values are not
allowed here') — which would have silently broken skill discovery, the
one thing the description exists to drive. Quoting the value fixes it.
Caught by parsing the frontmatter with yaml.safe_load rather than eyeballing
it; the same latent defect exists in server-management's canonical jellyfin
SKILL.md and is fixed there separately.
Skills/docs only, 0 C# lines, so the independent-review rubric's code triggers (locks, auth, write handlers, migrations, >150 C# lines) do not apply; reviewed by self-review against sources. Adversarial pass found and fixed one real defect: the reconciled description embedded 'IMPORTANT: ' which made the YAML frontmatter unparseable (mapping values are not allowed here) and would have silently broken skill discovery - caught by yaml.safe_load, not eyeballing, and the same latent defect was fixed in server-management's canonical jellyfin SKILL.md that this PR symlinks to. Also verified: no content dropped from the old copy across 22 key terms, symlink committed as mode 120000 with the correct relative target, and every corrected claim re-checked against a decision record or the live prod API.
Review-verdict: MERGEABLE @ 01fb07a
Skills/docs only, 0 C# lines, so the independent-review rubric's code triggers (locks, auth, write handlers, migrations, >150 C# lines) do not apply; reviewed by self-review against sources. Adversarial pass found and fixed one real defect: the reconciled description embedded 'IMPORTANT: ' which made the YAML frontmatter unparseable (mapping values are not allowed here) and would have silently broken skill discovery - caught by yaml.safe_load, not eyeballing, and the same latent defect was fixed in server-management's canonical jellyfin SKILL.md that this PR symlinks to. Also verified: no content dropped from the old copy across 22 key terms, symlink committed as mode 120000 with the correct relative target, and every corrected claim re-checked against a decision record or the live prod API.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
fixes #617
What was wrong
.claude/skills/jellyfin/was a real directory holding a stale fork of the canonical server-management copy — not the symlinkCLAUDE.mddescribes. It presentedX-Emby-Tokenas the auth pattern (v12 only accepts it viaEnableLegacyAuthorization, slated for removal in v13) and omitted theDELETE /Items/{id}data-loss warning that has destroyed files three times.Correction to the issue's premise
The issue comment framed server-management's
ersatztvcopy as "stale and dangerous" and ersatztv's as "correct". That is half right, and acting on it literally would have lost content:description:frontmatter (the "CRUD requires SQLite DB scripting" claim). Its body already said "Full REST CRUD is available under/api/v1; prefer it over direct DB writes".So the reconciliation takes server-management's file as the base and ports ersatztv's unique content onto it — not the other way round.
Changes
jellyfin— directory deleted, replaced with a relative symlink to~/server-management/.claude/skills/jellyfin, matching the pattern server-management already uses forbeets/radarr/sonarr/yt-dlp.ersatztv— reconciled as the canonical copy (ersatztv owns it perCLAUDE.md→ Project Boundaries):description:— it now says the REST API has write paths and SQLite is a fallbackapi.key401 trap, the lying OCI labels (#350), the local-vs-UTC log timestamp trap, thedocs/endpoint-index.mdpointer,settings/ffmpegordering, the scoped test-image refreshpageNum, per-endpointpageSizecap, and thatplayout/resettakes a channel idStale claims corrected in both copies (found while reconciling, verified against the decisions catalog):
auto_update: false, manualDeployStack jazz-mediaonly, no 03:00 fallback — the old text contradicted the fork's ownCLAUDE.mdGET /api/v1/media-sources. Also noted these are ErsatzTV ids, not Jellyfin'sPOST /api/v1/libraries/{id}/scanis the scan endpoint (there is noPUT …/sync)WatermarkSelectorFile.Exists()gate was removed in #502 (ffmpeg.external-logo-graphics-engine), logos cached at save time (graphics.channel-logo-caching)iptv.base_url(iptv.base-url)v26.8.0image reference (current release is v26.12.0)CLAUDE.md— both boundary lines now describe the actual arrangement, including the worktree caveat below.Known caveat
A relative cross-repo symlink resolves only in a checkout at
~/ersatztv, so thejellyfinskill is unavailable inside a git worktree. This is inherent to the pattern server-management already uses; documented inCLAUDE.mdrather than diverging to absolute paths (deviation policy).Follow-up
The server-management side (
~/server-management/.claude/skills/ersatztv→ symlink into this repo) lands as a separate commit in that repo after this merges, so the symlink target already holds the reconciled content.Verification
~/ersatztvto the canonical jellyfinSKILL.mddocs/decisions/README.mdrecords or live prod API, not memory.claude/means the review-verdict auto-pass does not apply, so a real verdict is required.`.claude/skills/jellyfin/` was a real directory holding a stale fork of the canonical server-management copy, not the symlink CLAUDE.md described. It documented `X-Emby-Token` as *the* auth pattern (v12 only accepts it via `EnableLegacyAuthorization`, slated for removal in v13) and omitted the `DELETE /Items/{id}` data-loss warning that has destroyed files three times. Replaced with a relative symlink, matching the pattern server-management already uses for beets/radarr/sonarr. `.claude/skills/ersatztv/` had diverged in BOTH directions, so a replace in either direction would have lost correct content. Contrary to the issue's framing, server-management's copy was stale only in its `description:` — its body already said "Full REST CRUD is available under /api/v1" and carried ~150 lines the ersatztv copy lacked (backup/deploy, test/prod topology, streaming-mode analysis, VAAPI-vs-QSV, table schema notes, checklists). So that file is the base, reconciled here as the canonical copy: - fixed the dangerous `description:` claiming CRUD requires SQLite scripting - ported ersatztv-only content: the silent root-owned api.key 401 trap, the lying OCI labels, the local-vs-UTC log timestamp trap, the endpoint-index pointer, `settings/ffmpeg` ordering, scoped test-image refresh - added the #616 paging facts (0-based pageNum, per-endpoint pageSize cap, playout/reset takes a CHANNEL id) per the request on #617 - corrected prod auto-update: `auto_update: false`, manual DeployStack only, no 03:00 fallback (contradicted the fork's CLAUDE.md) - corrected the ErsatzTV library ids: Music Videos is 16 and Standup 14, not 8/9 — verified live via GET /api/v1/media-sources - unpinned the stale v26.8.0 image reference (current release is v26.12.0) fixes #617The reconciled description ends with 'IMPORTANT: the fork has a full versioned REST API...'. An unquoted YAML scalar containing ': ' parses as a mapping, so the frontmatter failed to load ('mapping values are not allowed here') — which would have silently broken skill discovery, the one thing the description exists to drive. Quoting the value fixes it. Caught by parsing the frontmatter with yaml.safe_load rather than eyeballing it; the same latent defect exists in server-management's canonical jellyfin SKILL.md and is fixed there separately.Review-verdict: MERGEABLE @
01fb07aSkills/docs only, 0 C# lines, so the independent-review rubric's code triggers (locks, auth, write handlers, migrations, >150 C# lines) do not apply; reviewed by self-review against sources. Adversarial pass found and fixed one real defect: the reconciled description embedded 'IMPORTANT: ' which made the YAML frontmatter unparseable (mapping values are not allowed here) and would have silently broken skill discovery - caught by yaml.safe_load, not eyeballing, and the same latent defect was fixed in server-management's canonical jellyfin SKILL.md that this PR symlinks to. Also verified: no content dropped from the old copy across 22 key terms, symlink committed as mode 120000 with the correct relative target, and every corrected claim re-checked against a decision record or the live prod API.