merge: link silo upstream history and sync admin UX - #175
Merged
Conversation
…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>
…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>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 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. Comment |
JonahMMay
marked this pull request as ready for review
August 28, 2026 14:18
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>
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
PR #174 squash-merged ~413 silo commits onto old prairie
main, which imported the code but left GitHub showing 452 commits behindSilo-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
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.mainwith a real merge commit (--no-ff), not squash.PrairieBrand, vitest coverage CI config, renovate npm bumps from chore(deps): update npm (non-major) #170.After this PR,
mainis 0 commits behind silo and ~438 ahead (prairie customizations).Validation
CI pending on this branch.
Risks
AI Disclosure
Checklist