Skip to content

merge: link silo upstream history and sync admin UX - #175

Merged
JonahMMay merged 473 commits into
mainfrom
cursor/merge-silo-upstream-d673
Aug 28, 2026
Merged

merge: link silo upstream history and sync admin UX#175
JonahMMay merged 473 commits into
mainfrom
cursor/merge-silo-upstream-d673

Conversation

@JonahMMay

Copy link
Copy Markdown

Problem

PR #174 squash-merged ~413 silo commits onto old prairie main, which imported the code but left GitHub showing 452 commits behind Silo-Server/silo-server:main. The fork graph was never linked, so upstream tracking was meaningless and ~35 silo commits landed after the sync cut were still missing.

Related issue: N/A — fixes upstream sync process broken by #174 squash merge.

Approach

  1. Merge latest silo/main (including feat(admin): redesign admin settings — status overview, row-based sections, provider split Silo-Server/silo-server#708 admin settings UX) into the existing prairie sync branch that already sits on silo's history.
  2. Merge that branch into main with a real merge commit (--no-ff), not squash.
  3. Preserve prairie-only work: Live TV wiring, trickplay library toggles, PrairieBrand, vitest coverage CI config, renovate npm bumps from chore(deps): update npm (non-major) #170.
  4. Drop stale per-page admin settings files superseded by silo's consolidated UX.

After this PR, main is 0 commits behind silo and ~438 ahead (prairie customizations).

Validation

git rev-list --count HEAD..silo/main   # 0 (behind)
git rev-list --count silo/main..HEAD   # 438 (ahead)
make build                             # pass (Go + web)

CI pending on this branch.

Risks

AI Disclosure

  • Tool(s): Cursor Cloud Agent
  • Model(s): Claude (agent)
  • Involvement: AI-assisted, human requested
  • Adversarial review: n/a (merge/reconciliation PR)

Checklist

  • I read and can explain the complete diff.
  • This pull request addresses one concern.
Open in Web Open in Cursor 

Quick104 and others added 30 commits August 20, 2026 16:05
…dentials

fix(storage): require explicit S3 credential replacement
Address review findings on the credit-enrichment artwork guard.

photo_path, photo_source_path, and photo_thumbhash describe one image, but
each column was gated on its own value. A credit carries a photo URL and never
a source path, so replacing the "-" no-photo sentinel rewrote photo_path while
leaving the previous source URL in place. photo_source_path is what
UpdatePhotoIfSourceMatches keys the image-cache handshake on and what
EnqueueExistingProviderArtwork downloads from, so the finished job landed the
*old* image on the row, under the old image's thumbhash. All three columns now
move together under one decision taken on photo_path.

Deferring every replacement to the full person refresh also stranded people
with no tmdb/imdb/tvdb id: FindRefreshCandidates skips them, so nothing would
ever revisit a photo URL that had gone dead. The guard now protects cached
artwork specifically rather than any populated value — an empty column, the "-"
sentinel, and an uncached provider URL stay replaceable. "Not a cached key" is
the same LIKE '%://%' test the artwork GC trigger and the image cache sweep
use, so displacing a URL still queues nothing for deletion. Replacement
requires a genuinely different path, so re-scanning an unchanged credit remains
a no-op.

Tests: the SQL-shape test now matches whole generated clauses instead of loose
fragments, so a mis-wired column fails it, and the Postgres-backed test no
longer calls t.Fatalf on the parent T from inside a subtest. New cases cover
the stale-source binding, uncached-URL replacement, and the unchanged-credit
no-op. The behavioral coverage still needs SILO_TEST_DATABASE_URL, which CI
does not set.

Also build the batch enrichment SQL once instead of per batch.

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

fix(catalog): preserve cached person artwork during enrichment
… skill

AI-written PR and issue bodies routinely arrive padded with filler and
promotional framing that costs review time. Vendor the unslop skill into
.claude/skills/ so contributors' agents pick it up in-repo, and add a
Prose pass section to docs/ai-contributions.md making the pass an
expectation. Worded explicitly as readability, not concealment: it may
not alter facts, pasted output, or logs, and disclosure still applies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ster-hover-overlay

fix(requests): fill poster hover overlay
…ug-management-446d

fix(recommendations): do not panic when the engine is disabled
Create already leaves admins ungrouped because playback and catalog
policy is role-blind. Update did not: promoting a Default Group member
kept that group's stream cap and library list. Drop the group on
promote, reject assigning one to an existing admin, and ignore an
explicit group on admin create.

Co-authored-by: Quick <Quick104@users.noreply.github.com>
Review follow-up for the promote-clears-group fix. The rule now has one
write-side owner and one read-side guard instead of five copies:

- UserRepository.Update clears the group on promote and lands a demoted
  admin on the default group unless the write names one, so an ex-admin
  never becomes an uncapped non-admin.
- access.EffectivePolicyForUser ignores any group an admin row still
  carries (GroupApplies), covering every write path and pre-existing data.
- A data migration clears admins grouped before this rule and bumps their
  policy revision.
- PUT /admin/users/{id} rejects role=admin + access_group_id with 422
  whether the role is echoed or not, matching POST /admin/users; the
  handler no longer pre-clears the group itself.
- Invitations reject admin + access_group_id at send (422) instead of
  storing a group that accept silently drops.
- Web forms derive access_group_id=null for admins at submit; the detail
  form no longer wipes the picked group on a role toggle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up:

- A group written without a role change is resolved against the row's
  current role inside the UPDATE, so a write racing a promotion cannot
  leave an admin grouped; the migration also adds a
  users_admin_ungrouped CHECK constraint as the durable backstop.
- GET /admin/users applies the same GroupApplies guard as the detail and
  auth endpoints, so a legacy grouped admin row never reports group
  ceilings anywhere.
- Regression test for toggling the role to admin and back keeping the
  picked group.
- Lint: spelling, wasted assignment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dmin invitations

Review follow-up:

- Drop the CHECK constraint from this release's migration: in a rolling
  upgrade, previous-version nodes still promote without clearing the
  group and the constraint would surface as a 500. The repository's
  in-statement CASE and the read-side guard already hold the invariant;
  add the constraint once every writer is on this version.
- The migration also clears the group on still-pending admin invitations
  created before this rule, so they advertise what accept will do.
- Both user forms preview the no-group policy while Admin is selected
  instead of the retained group's ceilings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ug-management-cc92

fix(access): ungroup accounts promoted to admin
Co-authored-by: OpenAI Codex (GPT-5) <codex@openai.com>
Co-authored-by: OpenAI Codex (GPT-5) <codex@openai.com>
clientip.Middleware overwrites r.RemoteAddr with the header-derived viewer
address whenever the TCP peer is a trusted proxy, which includes Docker's
bridge. Mounting it on the ABS listener therefore defeated the login limiter's
deliberate RemoteAddr-only keying: an attacker behind any reverse proxy could
rotate X-Forwarded-For and buy a fresh burst bucket per request.

The middleware now preserves the pre-overwrite peer address in the request
context, and the limiter reads that instead. Anything else that must key on an
address a client cannot forge should do the same.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Compat attached observations under PlaybackSession.ID while the proxy,
nodesessions and playback_sessions_sync all key on playback.Session.ID, and
BuildGlobalView merges by exact SessionID string. One Jellyfin viewing therefore
showed as two merged sessions — a byte-less compat twin and the proxy record
carrying the traffic — and every compat session looked telemetry_only in parity.

Compat now attaches only under UpstreamSessionID. A play session does not learn
that id until ensureUpstreamPlayback/ensureTranscodeManifest has run, so the
pre-side-effect attach is a no-op on a session's first request and the handler
attaches again the moment the id exists, still before any byte is written. A
provisional key was rejected deliberately: it recreates exactly the ghost session
this fixes, and a session whose id did not exist a moment ago cannot have a
pending cut against it.

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

Three defects in the process-local registry, all found by review:

Truncated was sticky for the process lifetime. drop() set it and nothing ever
cleared it, so one transient capacity burst pinned the merged view's Complete
to false until a restart and made a later real truncation indistinguishable. It
now decays over Freshness — the same horizon BuildGlobalView uses to decide a
publisher is current — while the monotonic Dropped* counters keep the permanent
record.

SetRealtimeConnection was a no-op when the session did not exist yet. That is
the normal client ordering: the control socket opens as soon as a sessionId
exists, before the first media route is hit, so RealtimeConnectionAlive stayed
false for the whole of every live session. State for an unknown session is now
held per shard, applied when an attach creates the session, capacity-bounded
against the session budget, and pruned by the sweep.

The distributed cross-checks compared an env-supplied value against the DEFAULT
of the other knob, so setting one variable disabled distributed mode and blamed
a variable the operator never set. Knobs left at their defaults now move to
satisfy the invariant; only a pair pinned to genuinely inconsistent values is an
error, and only the variables actually set are named.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bumpStep throttle was written for the 32 KB Write path, where one
SetWriteDeadline per chunk would be wasteful. Applying it to ReadFrom slices
buys nothing — a slice is already bounded at 4 MiB — and costs correctness: a
slice completing less than a step after the last bump got no refresh, so the
next one started with as little as window-step remaining. The real guaranteed
floor was ~203 kbit/s, not the 186 kbit/s the constant and both design documents
promise, and a client sustaining the documented rate was reaped as stalled.

Slices now bump unconditionally, before the first as well as between each, which
is what the pre-CopyChunked loop did. Costs at most one syscall per 4 MiB.

The existing deadline tests construct the writer with step=0 and so never
exercised the throttle; the two added here fail on the unfixed code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
meteredResponseWriter previously hid io.ReaderFrom on purpose, so every byte
reached egressMeter.Add through a ~32 KB Write. Forwarding ReadFrom restored
sendfile but moved crediting to once per completed 4 MiB slice, which a
200-500 kbit/s direct-play viewer takes 60-170 s to fill. RateKbps averages over
60 s, so those streams read as zero for most samples: /api/v1/status
under-reports committed egress and nodepool's effectiveEgressKbps can admit
sessions onto a saturated proxy.

Metered slices are now 256 KiB — a credit every 4-10 s at those rates, well
inside the window, and still 8x more per sendfile call than the Write path it
replaced. Slice size here is a rate-fidelity constraint, not a tuning knob.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
handleDirectDownload passed the raw ResponseWriter to ServeDirect, so unlike the
sibling /downloads/{id}/file it had no rolling deadline and the API server's
absolute 120 s WriteTimeout truncated any original large enough to take longer.
Excluding the route from compression made it one unbounded sendfile, so the
whole body now rides on that single deadline.

redirectDirectDownload hardcoded an empty profile id in both the proxy redirect
and the telemetry attach, while the local branch two lines away reads the real
one. Proxy-served traffic was therefore missing from per-profile attribution in
telemetry, in the stream token claim and in the node session.

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

Transfers were one record per HTTP request keyed by observation id, so ranged
byte routes — audiobook file reads, download resumes, ebook fetches — could
exhaust MaxTransfers within one retention window while RequestCount, the field
that exists to count exactly this, stayed pinned at 1. A transfer is now one
subject pouring one file over one route, and overlapping requests fold into it.

A delta publish rewrites only changed fields and assumed the Redis hash still
held the rest. An eviction, an out-of-band DEL, a replica failover or a lapsed
PExpire drops it with no error, leaving under-reported sessions for up to
FullResyncEvery publishes. An HLEN inside the same transaction now catches the
mismatch and forces the next publish full, self-healing in one sweep.

recordConflicts appended started_at_replaced without setting
hasIdentityConflict, so the exported flag could disagree with the exported list.
A pure authority upgrade that confirms the recorded instant now records nothing
at all — it is not a conflict and should never have consumed the budget — and a
replacement that moves the value sets both.

Also documents two limitations rather than half-fixing them: clock skew is only
detectable for a publisher running ahead, since the roster score is the
publisher's own clock; and observedWriter.ReadFrom samples the cut flag once,
which the enforcement change that first calls cut.Store has to make uniform
across h1 and h2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nine ResponseWriter wrappers across five packages hand-rolled the same tail:
assert the inner writer's io.ReaderFrom, CopyChunked through it, fall back to
io.Copy over WriterOnly. Because io.Copy finds ReaderFrom by direct assertion
and never through Unwrap, this forwarding is mandatory on every media-route
wrapper — so a fix to it had to be re-applied nine times and a missed site
silently dropped to the fallback, losing zero-copy sendfile along with that
wrapper's byte accounting.

Behavior is unchanged; each call site keeps its own chunk size and record
callback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four families built the same clientip-then-RemoteAddr fallback chain inline
while streamtelemetry already had it unexported; a fix to it (IPv6 handling,
say) would have had to land in four places or the families would report
different viewer addresses into the same merged view. Exported as ViewerIP and
adopted everywhere.

envEnabled was the ninth independent "is this env var truthy" parser in the
tree, each accepting slightly different spellings. Adds internal/envutil and
adopts it in both telemetry packages; the remaining copies should migrate as
the code around them is touched.

checkVersion re-parsed every record into a throwaway header struct before
unmarshalling it again into a wire type that already carries the version, so a
merged-view rebuild — measured at ~347 ms for 50 000 sessions, nearly all
decode — did the JSON work twice.

ConfigFromEnv ran twice at startup because the view cache re-read the
environment just to get ViewTTL, logging any invalid variable twice; it now
takes the TTL off the registry that already parsed it.

playbackClientInfoFromRequest wrapped PlaybackClientInfoFromRequest wrapped
playback.ClientInfoFromRequest — three names, one body. Callers now use the
playback package directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Quick104 and others added 24 commits August 28, 2026 08:42
…rs page

Watch providers are pluggable, so the admin page can no longer pretend the
world is Trakt and Simkl: installations carrying a watch_sync_provider.v1
capability now render as tiles beside the built-ins, with enabled state and
a Configure action that deep-links to the plugin's page, plus a footer link
to the plugin catalog. The page and its nav entry are renamed Watch
Providers; the route id stays watch-sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…table input

SecretField's Configured/Replace/Keep three-state control becomes one
password input: a saved secret shows as a masked placeholder, typing stages
a replacement, and emptying the input keeps the saved value via onKeep
(form.resetValue) so a dirty "" can never erase a stored credential on
save — clearing stays a page-level action. The per-page replacement state
machines (Infrastructure's editor set, the SMTP flag) go away with it.
ConnectionCheckAction also gains the standard row padding so Check
Connection no longer hugs the hairline below it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ttings

Admin settings detail pages now render in the same surface-panel-lg shell
as the user settings page: a SideNavItem rail of all eleven pages on the
left (filterable by the shared SettingsSearchInput, without capturing the
admin's ⌘K), content on the right, All settings above. FieldGroup becomes a
thin wrapper over the shared SettingsGroup panel — restart-all as its
description line, a violet dot for unsaved edits — and SettingField rows
can carry their server_settings key as a mono caption plus a dirty dot.
Groups sit on a new surface-panel-raised step (hairline + raised fill,
high-contrast aware) so panels read as layers instead of floating,
especially in light themes where shell and surface are near-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reviewer feedback batch on the admin settings redesign, plus two
reported web regressions:

- Shell: settings search moved into the rail it filters; restart banner
  now in-flow at the top of every admin page (owned by AdminLayout);
  unsaved edits warn on navigation (createBrowserRouter migration +
  useBlocker guard, dirty registry in useSettingsForm).
- Alignment contract: label/control/unit row zones with a reserved unit
  slot and per-kind control widths, applied across all twelve pages;
  double hairlines and ad-hoc subheadings unified.
- Pages: Downloads split out of Playback into its own section (legacy
  alias removed); marker providers moved onto the providers page as
  tiles with honest connected-state and a deep link that opens the
  plugin's configure dialog (?configure=); per-user vs whole-server
  download limits grouped; storage budget in GB; chapter thumbnails
  gated on transcode-node availability; path settings show
  effective-default placeholders with reset-to-default.
- Providers: two-wide tiles with aligned bottom rows, right-aligned
  panel actions, Trakt client ID hot-reloads (restart badge removed),
  AI feature toggles gated on provider readiness.
- Appearance: accent picker always marks the active choice (default
  swatch, ring, custom chip); hover-intent on theme preview; bundled
  default logos with pipeline-derived upload guidance; overlay preview
  in a framed strip with a movie/show toggle; card overlay
  restore-defaults on both surfaces (user reset is a true clear so
  profiles inherit future server defaults); bottom overlay badges sit
  flush in corners with quick actions painting above them.
- Users/security: /admin/users tabs are URL-addressable with a
  public-signups badge and invite-codes deep link; Redis rate-limit
  option gated on redis_available; limiter drift hints restored inline.
- Web perf: hot route chunks prefetch after auth, item-detail reveal
  gate skips when cached, realtime section invalidation scoped and
  debounced, Home section/layout cache kept alive; library saved-view
  no longer flashes Recommended (startTransition batching).
- Backend: chapterthumbs and scanner ffmpeg consumers read settings
  live; EffectiveDownloadArtifactDir cleans a trailing-slash transcode
  dir so the artifact root cannot nest where the orphan sweep would
  delete prepared downloads; route manifest regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review-bot findings, all verified against the code before fixing:

- Batch settings PUT validates image caching against the complete
  prospective state: an explicit empty bucket value is a clear, not an
  absence, and clearing public storage while metadata.cache_images stays
  enabled is rejected (both reviewers' variants; five regression tests).
- Internal admin routes use router Links instead of full-page <a> loads;
  the search shortcut hint is platform-aware via one shared helper.
- Strict integer parsing for the transcription quota; clipboard copy
  confirms before claiming success; search-status failures render
  visibly; a mistitled log-retention test says what it asserts.
- Legacy /admin/autoscan?tab= deep links translate to the embedded
  page's view parameter through a shared URL-contract module.
- The Storage overview card distinguishes "restart pending" from "not
  set up" when a bucket is saved but not yet live.
- Security & Access saves its two writers sequentially (settings batch
  first) so the rate-limit backend is validated against the intended
  state, and its rate-limit draft registers with the unsaved-changes
  registry — which now also owns a global beforeunload prompt.
- Home sections are marked stale (without refetching) on library-scoped
  catalog events so the throttled refresh fetches real data instead of
  re-rendering the outdated cache.
- The restart banner re-arms after "Later" via a new restart_mark_count
  on the server status response: the boolean latches for the process
  lifetime, so the count is the only signal of a new requirement.
- goconst/errorlint findings on branch-touched lines: error-code
  constant extracted, errors.Is comparison, and a reasoned nolint on the
  settings defaults data table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in tile state on config

Two review findings:

- Provider tile drafts (subtitle credentials and enable switch, the
  MDBList key, marker provider behavior edits) live outside
  useSettingsForm, so the navigation guard and reload prompt never saw
  them. Each tile now reports its draft to the unsaved-changes registry.
- Watch-provider plugin tiles read "connected" for any enabled
  installation, even with a required-but-empty global config. Readiness
  now derives from the declared schema and saved values via a shared
  installationConfigReady helper (extracted from the marker tiles, which
  already enforced this), and the Configure action deep-links into the
  plugin's configure dialog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- docs/admin-api.md: scope statement admits the deliberately public
  branding reads, and the asset kinds include the light variants.
- Scanner probe/copy-safety flows capture one binaries() snapshot per
  operation, so a concurrent SetFFmpegPath("") can no longer hand the
  worker an empty binary path between guard and use.
- Empty light-logo slots preview the main asset they actually fall back
  to (new fallbackUrl on BrandingAssetField) instead of a gradient.
- Marker task rows track pending state per task; one completion no
  longer re-enables the other row mid-run.
- Overlay editors are inert while overlays are disabled server-side, so
  keyboard activation cannot edit through the pointer-events guard.
- Settings-row separators have one owner: rows rule themselves and the
  list rule now covers only non-row blocks, removing double hairlines
  inside Advanced disclosures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t, clear css draft on save

Third-round review findings:

- The restart tracker accumulates one "setting:<key>" reason per
  restart-required save and exposes the list on the status response, so
  the overview scopes "Restart pending" to the tile whose keys actually
  changed — a database save no longer warns the Transcoding tile, and a
  later unrelated save cannot erase the evidence of a pending playback
  change. Older single-reason servers fall back to the coarse heuristic.
- GET /admin/rate-limits/config builds the whole response from one
  settings snapshot (ConfigFromSettings over a single GetAll), so rate
  values and capability fields cannot straddle an update.
- The Appearance page drops its raw CSS draft after a successful save,
  so the editor shows the sanitized canonical value instead of stripped
  content that looks accepted; a failed save keeps the draft.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…st Discord configured state

- Trakt/Simkl tile drafts and the Discord application card's credential
  drafts now report to the unsaved-changes registry, so the navigation
  guard and reload prompt cover them like every other draft.
- The page-level Discord "configured" flag mirrors the server's rule
  (client id AND client secret AND bot token) instead of only the bot
  token, so a partial save cannot read as connected while account
  linking and delivery remain unavailable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A required plugin config saved as an explicit boolean false (e.g.
{"advanced": false}) is a value the admin deliberately chose, so the
readiness predicate no longer treats it as unfilled — a runnable plugin
cannot read "Needs setup". Only a blank string stays excluded: an empty
text box is not a value, which is what keeps a keyless plugin from
reading connected. Dedicated unit tests pin the contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…restart-status API

- PUT /admin/settings/{key} now treats image caching's bucket like the
  Redis transport: a durable prerequisite a single-key write may not
  break. Clearing s3.public_bucket (or the legacy key) while
  metadata.cache_images is stored on is rejected with
  storage_unavailable — disable caching first. Regression tests cover
  clear-while-on, clear-while-off, and change-while-on.
- docs/admin-api.md documents GET /admin/server/status (including
  restart_required_reasons and restart_mark_count and why each exists)
  and GET /admin/settings/restart-keys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…upe test helper

- The status handler derives the Jellyfin web-component restart
  requirement outside the tracker, so the accumulated reasons list now
  gains "jellyfin_compat" on that path (deduped) — a client scoping
  restarts by reason can see it.
- updateCacheImagesSingle delegates to updateSingleSetting instead of
  duplicating the router setup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s a sender

- SecretField gains an explicit "Clear saved value" affordance (onClear/
  cleared props): staged through the save bar with a visible
  "Will be cleared on save" state and a "Keep saved value" undo. Wired
  only where no page-level clear exists — Meilisearch API key, S3 key
  pairs (which must clear together, per server validation), the public
  token secret, SMTP password, and the AI keys (the text-model clear
  also clears the legacy subtitle_ai.api_key so the fallback cannot
  silently keep the old secret in force). Emptying the input still means
  "keep"; env-managed rows never offer the action. useSettingsForm owns
  the staged-clear notion via isClearStaged.
- Mail readiness mirrors the server's rule via one shared helper: the
  switch, a host, AND a sender address. Both the Notifications page and
  the overview tile now refuse to call enabled-without-sender "Ready" —
  a state legacy rows and single-key writes can still store.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Skipping the home refresh signal for progress events entirely left an
open home page blind to another client's playback: Home renders its own
loaded-section state, so marking queries stale never updates the visible
Continue Watching bar, and a first tick changes section membership.
Progress events now arm one trailing refresh per 30s window — home
catches up without the per-tick load-queue storm the skip existed to
prevent. Membership changes still refresh immediately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The relay URL draft lives outside useSettingsForm (only the registration
endpoint persists it), so navigating away or reloading discarded it with
no prompt — unlike every other credential draft on the page. The draft
now reports to the unsaved-changes registry, placed above the loading
return so the hook stays unconditional.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dmin-settings-ux

feat(admin): redesign admin settings — status overview, row-based sections, provider split
Merge latest silo upstream (35 commits since the #174 sync cut) into the
prairie sync branch. Accept silo's simplified admin settings UX deletions,
keep prairie Go handlers and Live TV wiring, and restore prairie branding
in merged web tests/specs.

Co-authored-by: Jonah May <JonahMMay@users.noreply.github.com>
Merge the prairie sync branch (silo-based history + prairie rebrand) into
main with a real merge commit so GitHub tracks upstream correctly. Brings
in silo Silo-Server#708 admin settings UX and the 35 commits since the #174 cut
while preserving prairie-specific server wiring.

# Conflicts:
#	contracts/settings/v1/conformance.json
#	contracts/settings/v1/manifest.json
#	docs/admin-api.md
#	internal/api/testdata/media_routes.txt
#	internal/chapterthumbs/service.go
#	internal/chapterthumbs/service_test.go
#	internal/config/admin_settings.go
#	internal/config/config.go
#	internal/config/restart_keys.go
#	internal/scanner/probe_repair.go
#	internal/scanner/probe_repair_copy_safety_cached_test.go
#	internal/settingskeys/keys.go
#	web/package.json
#	web/pnpm-lock.yaml
#	web/src/App.tsx
#	web/src/app.css
#	web/src/components/AdminLayout.tsx
#	web/src/components/AdminSectionCommandDialog.test.tsx
#	web/src/components/AdminSectionCommandDialog.tsx
#	web/src/components/AdminSidebar.test.tsx
#	web/src/components/AppSidebar.test.tsx
#	web/src/components/AppSidebar.tsx
#	web/src/components/CollectionTemplateGallery/MDBListBrowser.tsx
#	web/src/components/ContinueWatchingCard.test.tsx
#	web/src/components/ContinueWatchingCard.tsx
#	web/src/components/ItemCard.test.tsx
#	web/src/components/ItemCard.tsx
#	web/src/components/ItemGrid.tsx
#	web/src/components/Layout.test.tsx
#	web/src/components/Layout.tsx
#	web/src/components/MediaCarousel.tsx
#	web/src/components/MediaItemMenu.test.tsx
#	web/src/components/MediaItemMenu.tsx
#	web/src/components/RealtimeEventsProvider.tsx
#	web/src/components/SectionItemCard.test.tsx
#	web/src/components/SectionItemCard.tsx
#	web/src/components/SectionRow.tsx
#	web/src/components/ThemeSwitcher.tsx
#	web/src/components/admin/BrandingAssetField.tsx
#	web/src/components/admin/libraries/LibraryFormSections.tsx
#	web/src/components/mediaItemMenuTrigger.ts
#	web/src/components/overlays/CardOverlays.test.tsx
#	web/src/components/overlays/CardOverlays.tsx
#	web/src/components/overlays/OverlayPreviewCard.tsx
#	web/src/components/realtimeCatalogInvalidation.ts
#	web/src/components/settings/SettingSlider.tsx
#	web/src/components/settings/SettingsGroup.tsx
#	web/src/components/settings/SettingsSearchInput.tsx
#	web/src/components/sidebarItemNavigation.ts
#	web/src/hooks/queries/admin/settings.ts
#	web/src/hooks/queries/mediaSurfaceRefresh.test.ts
#	web/src/hooks/useLongPress.ts
#	web/src/hooks/useOverlayPrefs.test.tsx
#	web/src/hooks/useOverlayPrefs.ts
#	web/src/hooks/useTheme.tsx
#	web/src/lib/adminNavigation.ts
#	web/src/lib/adminSettingsSearch.ts
#	web/src/lib/queryInvalidation.test.ts
#	web/src/lib/queryInvalidation.ts
#	web/src/lib/settingsConformance.json
#	web/src/lib/settingsContract.ts
#	web/src/lib/uiCustomization.ts
#	web/src/pages/AdminAutoscan.tsx
#	web/src/pages/AdminDashboard.tsx
#	web/src/pages/AdminLibraries.tsx
#	web/src/pages/AdminPlugins.tsx
#	web/src/pages/AdminUsers.tsx
#	web/src/pages/Catalog.test.tsx
#	web/src/pages/Home.test.tsx
#	web/src/pages/ItemDetail/components/EpisodeCarousel.test.tsx
#	web/src/pages/ItemDetail/components/EpisodeCarousel.tsx
#	web/src/pages/ItemDetail/components/SeasonEpisodeGrid.test.tsx
#	web/src/pages/ItemDetail/components/SeasonEpisodeGrid.tsx
#	web/src/pages/LibraryPage.test.tsx
#	web/src/pages/LibraryPage.tsx
#	web/src/pages/LibraryRecommended.tsx
#	web/src/pages/Recommendations.tsx
#	web/src/pages/RecommendationsSection.tsx
#	web/src/pages/SettingsLayout.tsx
#	web/src/pages/admin-settings/AIServicesSettings.test.tsx
#	web/src/pages/admin-settings/AIServicesSettings.tsx
#	web/src/pages/admin-settings/AdminSettingsLayout.test.tsx
#	web/src/pages/admin-settings/AdminSettingsLayout.tsx
#	web/src/pages/admin-settings/BrandingSettings.tsx
#	web/src/pages/admin-settings/CompatibilityProxiesSettings.test.tsx
#	web/src/pages/admin-settings/CompatibilityProxiesSettings.tsx
#	web/src/pages/admin-settings/DatabaseSettings.test.tsx
#	web/src/pages/admin-settings/DatabaseSettings.tsx
#	web/src/pages/admin-settings/DownloadSettings.tsx
#	web/src/pages/admin-settings/EmailSettings.tsx
#	web/src/pages/admin-settings/FieldGroup.tsx
#	web/src/pages/admin-settings/GeneralSettings.tsx
#	web/src/pages/admin-settings/IntegrationsSettings.tsx
#	web/src/pages/admin-settings/IntroSettings.tsx
#	web/src/pages/admin-settings/InviteCodesTab.tsx
#	web/src/pages/admin-settings/LogRetentionSettings.tsx
#	web/src/pages/admin-settings/NotificationsAdminSettings.test.tsx
#	web/src/pages/admin-settings/NotificationsAdminSettings.tsx
#	web/src/pages/admin-settings/OverlaySettings.tsx
#	web/src/pages/admin-settings/PlaybackSettings.test.tsx
#	web/src/pages/admin-settings/PlaybackSettings.tsx
#	web/src/pages/admin-settings/RateLimitSettings.tsx
#	web/src/pages/admin-settings/SaveBar.tsx
#	web/src/pages/admin-settings/ScannerSettings.tsx
#	web/src/pages/admin-settings/SearchSettings.tsx
#	web/src/pages/admin-settings/SettingField.test.tsx
#	web/src/pages/admin-settings/SettingField.tsx
#	web/src/pages/admin-settings/StorageSettings.test.tsx
#	web/src/pages/admin-settings/StorageSettings.tsx
#	web/src/pages/admin-settings/SubtitlesSettings.tsx
#	web/src/pages/admin-settings/ThemeSettings.test.tsx
#	web/src/pages/admin-settings/ThemeSettings.tsx
#	web/src/pages/admin-settings/WatchProvidersSettings.tsx
#	web/src/pages/admin-settings/playbackSettings.utils.test.ts
#	web/src/pages/admin-settings/playbackSettings.utils.ts
#	web/src/pages/settings/AppearanceSettings.tsx
#	web/src/pages/settings/CardOverlaySettings.tsx
#	web/src/pages/setup-wizard/steps/ServerStorageStep.tsx
#	web/src/player/hooks/useCoarsePointer.ts
#	web/src/utils/storage.test.ts

Co-authored-by: Jonah May <JonahMMay@users.noreply.github.com>
Drop admin settings pages replaced by silo Silo-Server#708 UX, restore trickplay
library toggles and restart-status API fields, and fix LiveTV SaveBar
typing after the real merge into main.

Co-authored-by: Jonah May <JonahMMay@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b073dd28-cfb9-4834-91ce-5bb20d53c80a


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@JonahMMay
JonahMMay marked this pull request as ready for review August 28, 2026 14:18
cursoragent and others added 2 commits August 28, 2026 14:28
Restore prairie setup-wizard branding and Redis env guards lost in merge,
mock useQuery in ServerStorageStep tests, fix jsdom CardOverlays legacy
scaling assertion, replace stale overlay quick-action tests, fix silo import
in admin_restart_keys_test, and run prettier across the web admin merge.

Co-authored-by: Jonah May <JonahMMay@users.noreply.github.com>
The silo merge dropped ArtworkImage width ladders from ContinueWatchingCard,
breaking artworkRungWiring tests and oversized continue-watching stills. Restore
prairie artwork wiring and run prettier on ServerStorageStep.

Co-authored-by: Jonah May <JonahMMay@users.noreply.github.com>
@JonahMMay
JonahMMay merged commit 9492bb0 into main Aug 28, 2026
15 checks passed
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.

6 participants