Skip to content

S270b: the docs sweep followups — 13 verified-open items, rewritten against the code - #103

Merged
detain merged 3 commits into
masterfrom
s270b-sweep-followups
Aug 7, 2026
Merged

S270b: the docs sweep followups — 13 verified-open items, rewritten against the code#103
detain merged 3 commits into
masterfrom
s270b-sweep-followups

Conversation

@detain

@detain detain commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Rebased onto f1f942b (S270 Job 1) so its green is measured against the true base — the two branches overlap on 10 files and Job 2 alone still showed :32400 in two pages.

Denominator

14 in-repo items, 13 worked, 0 deferred (item 6 was Job 1). ~132 claims re-verified; 58 verified TRUE and left byte-identical.

The sweep's own list was incomplete in five places

Found only by going back to the code, not by trusting the list:

  • item 8 named 3 pages, but 17 carried the claim
  • item 9 was wrong in 2 places
  • item 12 had 5 occurrences, not 4
  • item 5 understated the fail-open — it is any non-array, not just omission
  • item 3's aud flag was wrong — the enrollment JWT is correct, so it was left alone

Worst finds

  • share-with-friends.md said an invite link "is tied to the exact email address it was sent to". The opposite is true — the share goes to whoever redeems it.
  • permission_level is never enforcedcanWrite() has zero production callers.
  • parental-controls.md claimed all three layers run on every playback request. Direct play is served before the router, so there is no bedtime cutoff on that path.
  • arr-integration.md told Radarr users to avoid root-folder moves over a defect S158 already fixed.

Gates

Clean detached worktree, every exit code from its own $?: npm test 0 (85 tests), docs:build 0.

pages links dead
control ca79f7a 158 3454 0
this branch 158 3452 0

The −2 is measured, not assumed: heading changes in share-with-friends.md (17→14), library-sharing.md flat, plus two new ### anchors confirmed present as real ids in the emitted HTML. Local and CI counts agree, so srcExclude is holding.

23 files.

🤖 Generated with Claude Code

Phlex Agent and others added 3 commits August 7, 2026 18:28
Nine of the sweep's fourteen in-repo items, each re-verified against the code at a
named file:line before any edit. Claims that verified TRUE were left byte-identical.

S158 MOVED-FILE BLOCK (sweep item 8) — the largest correction. S158 shipped
(`3378bb2c`, reachable from origin/master, 22 integration tests): the scan records
an adoption candidate and `LibraryManager::pruneRemovedItems()` calls
`adoptRecordedPath($id)` immediately before the DELETE, re-pointing the row instead.
The docs still told operators a plain move destroys watch state, and told Radarr
users not to change root folders. The sweep listed three places; there were
SEVENTEEN saying the same wrong thing, so all seventeen moved together — fixing
three would have left them contradicting fourteen.

Five sub-clauses stayed because they are still TRUE, and a blanket deletion would
have lost them: a plain `scan` job records nothing (it never prunes) so the stale
row survives and will not play; the standalone `prune` job still deletes; a move
that also RENAMES is a different canonical key and still lands a new UUID; the
20,000-candidate cap; and the 2,000-entry probe budget.

like_level (item 9) — documented `0-3`, actually `-2..2` (`MIN_LIKE`/`MAX_LIKE`),
a signed thumbs axis. Wrong in TWO places, not the one the sweep found. Both 400
bodies differ and are now quoted exactly.

Catalog source duplicate (item 10) — documented as a 200 no-op; the service throws
`InvalidArgumentException(..., 409)` twice and the controller maps it to
`plugin.catalog.url.duplicate`. Added the 409 response; URLs are compared after
normalisation. No prose page claimed otherwise, so this is spec-only.

"Immutable default catalog URL" (item 11) — OVERSTATED, not false: "cannot be
removed" is TRUE (`removeSource()` only filters extras, `sources()` always prepends
the default) but the value resolves from the `plugins.catalog.default_source`
setting. `docs/plugins/plugin-catalog.md:64` already said so — spec-vs-prose drift
again. Also recorded that no endpoint can change it: the key is absent from
`server-settings.schema.json`, so `PUT /admin/settings` rejects it.

Federation port (item 12) — 8804 -> 8805. FIVE occurrences, not the four the sweep
listed. 8804 is SyncPlay. Added the disambiguation, since `FederationWorker`'s own
docblock carried this exact error until S66.

MCP scopes (item 5) — the fail-open is wider than "omitting `scopes`": the guard is
`is_array($rawScopes)`, so null, a bare string, a number or a boolean all grant all
four scopes including the write scope. A typo INSIDE an array fails closed; a wrong
TYPE fails open. Also recorded that any authenticated user can mint one.

media-index offsets (item 13) — the claimed "server<->rail integration tests" do not
exist; the real tests use hand-written counts and a mocked ItemRepository. Also
documents that the offset invariant does not hold for `rating` or `date_added`, for
rows with no value, or when `minRating`/tag filtering diverge.

Plugin env vars (item 14) — `PHLIX_PLUGINS_ALLOW_UNVERIFIED` was undocumented in
both places `dev/plugin-sdk.md:489` requires. Four more were undocumented anywhere:
`PHLIX_GITHUB_TOKEN`, `GITHUB_TOKEN`, `GH_TOKEN`, `PHLIX_ANIDB_CACHE_DIR`. The rule's
own preamble said "three env vars"; `PluginsProvider` reads two.

Pairing protocol (item 3) — rotation is test-only (`rotate()` has no production
caller and no `scripts/rotate-hub-key.php` exists); claim codes are 8 chars not 6
(three places); the user-session `aud` is `phlix-server` not `server`, and §8 is
unimplemented hub-side; claim submission is NOT rate-limited; the `kid` is a SHA-256
fingerprint not a timestamp; enrollment state is one file, not two; the
per-server `heartbeat_interval` column is inert; and the keypair snippet split at
byte 32 instead of 64, which would produce a truncated key if copied.
NOT changed: the enrollment JWT's `aud: server` at :356 — the sweep flagged it, but
minting and validation both use `server`. It is correct.

Backup (item 4) — restore is NOT atomic: no transaction, no snapshot, and a config
failure after the mysql import leaves the DB replaced and config/ half-written.
Auto-backup requires an explicit `enabled` the sample config omitted, and the
schedule is only read at boot. Retention deletes the S3 object OR the local file,
never both — and uploading rewrites `file_path`, so an uploaded backup's local
archive can never be cleaned up. Five wrong response literals corrected.

Gates, each exit code read from its own `$?` on an unpiped command: `npm test`
exit 0 (5 files, 85 tests); `npm run docs:build` exit 0, anchor gate 158 page(s),
3456 unique fragment links, 0 dead. +2 vs the origin/master control (3454), both
from the outline links of the two new `###` headings (`#retention` in
admin/backup.md, `#the-17-endpoints` in admin/remote-access.md); both confirmed
present as real ids in the emitted HTML.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the real model

Sweep item P0.1. The access-schedule section the previous sweep already fixed is
left byte-identical; so are the day/time fields, the "first active match refuses"
rule, overnight ranges, and the fail-open note. Everything else was wrong.

The three security-relevant corrections:

1. "All three layers are evaluated together on every playback request" — FALSE, and
   the drawn pipeline does not exist. The access-schedule middleware is the only
   GLOBAL middleware, and direct play (`GET /media/{id}/stream`) is served BEFORE
   the router, so no schedule check applies to it at all. A profile inside a
   blackout window can still direct-play. Said plainly, because an operator relying
   on this for a bedtime cutoff does not have one.

2. Tag blocking was described as a playback gate that denies direct URLs. It is a
   row filter inside `ItemRepository` and never blocks a stream. It is also NOT
   applied to Continue Watching, Next Up or Search — those are built from playback
   state or take only the rating gate. And it is inert on any pre-router path,
   because the only writer of the request-context profile id is the access-schedule
   middleware.

3. The `allowed` tag list is subtractive, not additive: one allowed tag drops every
   item that lacks it. The page said user rules were "additive to — never
   subtractive from" admin rules; there is one store, not two tiers.

Also fictional, now removed: subscription tiers (Individual/Starter/Family/Premium
— no plan concept exists; the cap is one integer per profile, defaulting to 1),
household/family stream budgets, the 18 predefined tag categories (tags are
free-text `VARCHAR(100)` with no seed data), a per-schedule Timezone field, group
targeting, and the allow/deny Action column — which the page's own "Schedule
Priority" section already contradicted.

Both error bodies were invented. `ACCESS_SCHEDULED_DENIED`, `STREAM_LIMIT_REACHED`
and `next_available` have ZERO occurrences repo-wide. The real bodies are flat,
`403 {"error":"AccessScheduled",...}` and `429 {"error":"StreamLimitExceeded",...}`,
and neither carries the active-stream list a client was told to render — that needs
a separate call to `GET /api/v1/profiles/{id}/active-streams`.

One behaviour worth knowing that the page never mentioned: stream slots are
reclaimed on a 60-second heartbeat timeout, not when playback stops, so with the
default cap of 1 a viewer can be locked out of their own next title for a minute.

Anchor gate: 158 page(s), 3455 unique fragment links, 0 dead — net -1 against the
previous commit's 3456, from the tag section losing three `###` headings and gaining
two. `npm test` exit 0 (85 tests); `npm run docs:build` exit 0. Both read from `$?`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… against the code

Sweep item P0.2. These two pages are not invention: phlix-hub's own CHANGELOG (H.2)
describes exactly the UI they document — a "Share Library" modal with server,
library, collaborator email, permission and expiry; a "Libraries I've Shared" table
with inline permission editing; a "Shared With Me" page with a "Browse Library"
link. That UI was built on the server-rendered template stack, deleted with it, and
the Vue replacement re-implemented **list and revoke only**. The pages were left
describing the old one.

Security-relevant, and inverted:

  * "The invite is tied to the exact email address it was sent to." The OPPOSITE is
    true. Redeeming a link creates the share for whoever redeems it, under their own
    account email; a link is bounded only by max-uses and expiry, never by identity.
    Now documented as a bearer token, because a reader who believed the old sentence
    would forward a link thinking it was safe.

  * `permission_level` is stored, validated, returned and rendered as a badge, but
    **nothing enforces it** — `LibraryShare::canWrite()` and
    `SharedLibraryDto::canWrite()` have zero production callers and are pinned only
    by their own unit tests. Both pages now say the level is a label, not a control.

  * "DLNA casting requires View + Playback + Download." No permission gates DLNA,
    and that tier does not exist. The old troubleshooting entry told a user to ask
    for a permission upgrade that would have changed nothing.

Removed as non-existent: the entire email-invite flow (phlix-hub has **no mailer**
— `mail(` has zero hits repo-wide including vendor/, and the one `symfony/mailer`
reference in composer.lock is inside monolog's require-dev and is not installed);
three permission tiers (there are two, `read` and `readwrite`); folder-level and
item-level scopes (a share carries one `library_id`; there is no sub-library
plumbing); and the "Restrict to G-rated content" filter (zero occurrences — the
rating cap is a media-server profile feature with no wiring to hub shares).

Corrected: Shares and Shared With Me are two pages, not one; the shares table has
six columns, not five; "Shared with" shows a display name, not an email, because
the API never returns one; the button is "Open Server" and opens the media server's
own web UI, because the hub has no `/browse/:server/:library` route; invite expiry
has five options, not four; the audit-log greps named a file that does not exist
(`.logs/hub-audit.log`) and event strings that do not exist.

Left byte-identical because they verified TRUE: the legacy-redirect note, the
six-row API endpoint table, "collaborator must have a hub account", the
authentication note, "revoke takes effect immediately", "you cannot re-share",
"your access can be revoked at any time", and "there is no server-side CLI for
sharing". Dropped `**Since:** 0.19.0` from library-sharing.md — no repo has ever
been at 0.19.0 (hub is 0.5.0) and the real origin is hub milestone C.9.

Gates, each from its own `$?`: `npm test` exit 0 (85 tests); `npm run docs:build`
exit 0; anchor gate 158 page(s), 3452 unique fragment links, 0 dead. -3 against the
previous commit, verified rather than assumed: `share-with-friends.md` goes 17 -> 14
headings and `library-sharing.md` stays at 17 (one added, one removed). "Without a
fragment" +2, the two new cross-page links.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@detain
detain merged commit 961c44f into master Aug 7, 2026
2 of 3 checks passed
@detain
detain deleted the s270b-sweep-followups branch August 7, 2026 22:31
@codacy-production

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 1 medium · 1 minor

Alerts:
⚠ 2 issues (≤ 0 issues of at least minor severity)

Results:
2 new issues

Category Results
ErrorProne 1 medium
CodeStyle 1 minor

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant