Skip to content

merge: repair silo upstream ancestry after accidental squash - #176

Merged
JonahMMay merged 453 commits into
mainfrom
cursor/fix-silo-ancestry-d673
Aug 28, 2026
Merged

merge: repair silo upstream ancestry after accidental squash#176
JonahMMay merged 453 commits into
mainfrom
cursor/fix-silo-ancestry-d673

Conversation

@JonahMMay

Copy link
Copy Markdown

Problem

PR #175 was squash-merged, which dropped silo/main from main's ancestry. GitHub now shows prairie-server as hundreds of commits behind Silo-Server/silo-server even though the squashed tree already contains the upstream content.

Approach

Add a history-only -s ours merge of silo/main onto current main. This records silo as a parent without changing any files — git diff main..HEAD is empty. After merge, upstream tracking should read 0 behind silo again.

Merge instructions: use "Create a merge commit". Do not squash.

Validation

git merge-base HEAD silo/main   # => silo/main tip
git rev-list --left-right --count HEAD...silo/main   # => N 0 (0 behind)
git diff main HEAD --stat       # => (empty)

CI on squashed main (#175) is running; this PR should be a no-op for code and tests.

Risks

None for runtime behavior — zero file changes. Only git graph repair.

AI Disclosure

  • Tool(s): Cursor Cloud Agent
  • Model(s): claude-sonnet-5-thinking-xhigh
  • Involvement: AI-assisted
  • Adversarial review: n/a — history-only merge commit

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 13:19
Scoped API keys: an sa_ key may now carry an allowlist of scopes
(admin:users, admin:access-groups:read). A scoped key is refused on every
route its scopes do not name — including the Jellyfin-compat surface and
plugin access — so an integration credential (e.g. a billing system doing
user provisioning) no longer needs a full-power admin key. Empty scopes
keep the existing behavior. Scopes narrow, never grant: role checks still
apply to the owning user.

Admin user API hygiene:
- POST /admin/users maps a duplicate username/email to 409 duplicate
  instead of an opaque 500, so clients can distinguish a lost-response
  retry from a genuine server error.
- DELETE /admin/users/{id} maps a missing user to 404, making terminate
  retries idempotent from the client's point of view.
- POST /admin/users accepts access_group_id (the repository already
  supported it), removing the create-then-update window where a new user
  briefly sat under the default access group.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User policy fields move from "strictest of user and group wins" to
inherit/override: NULL on the user row means "inherit the access group's
value"; a set value is an explicit per-user override that replaces the
group value in either direction — grant or restrict. A group saying "no
downloads" no longer forbids granting downloads to one of its members,
and a member's cap above the group's now wins instead of being clamped.

- users: max_streams, max_transcodes, max_playback_quality,
  transcode_allowed, audio_transcode_allowed, download_allowed,
  download_transcode_allowed, library_ids become nullable (NULL =
  inherit); new nullable requests_allowed. Numeric 0 becomes an explicit
  "unlimited" override instead of a delegation sentinel.
- access_groups: gain transcode_allowed / audio_transcode_allowed so
  every user field has a group value to inherit.
- resolution collapses to effective = user.field ?? group.field ??
  permissive no-group default; permissions keep the allowed_permissions
  intersection mask. All enforcement reads go through the resolver,
  including the previously raw fallbacks (items/sections/libraries
  library scope, legacy permission middleware, /auth/me and login
  download_allowed, requests gate — which now honors a user override).
- downloads package no longer launders effective policy back into
  models.User; checks take a resolved PolicyUser.
- admin user API: PUT accepts explicit null per policy field to clear an
  override back to inherit (tri-state), GET reports stored overrides
  (null = inherited) plus a resolved effective_policy block; access-group
  API carries the two new gates.
- migration maps old delegate values (0 / '' / true) to NULL and keeps
  restrictive values as overrides, so existing behavior is preserved
  except the deliberate cap-above-group change.
- web admin: user forms get per-field Inherit/Override controls showing
  the inherited effective value, saves send explicit null for inherited
  fields (no more silent pinning on save), the user overview shows
  effective values with override provenance, and the access-group editor
  gains the video/audio transcoding toggles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Findings from the adversarial review of c025a6e6:

- An explicit empty library_ids override ([] = no libraries) round-trips
  as [] in admin responses instead of collapsing to null (inherit), so an
  admin open+save no longer silently deletes a deny-all override.
- effective_policy now fails closed: a failed access-group lookup returns
  500 instead of silently rendering a group-restricted user against the
  permissive no-group default.
- Migration maps a pre-existing NULL download gate (the columns were
  always nullable) to inherit instead of inventing an explicit deny
  override, and the down block documents its inherent lossiness for
  explicit permissive overrides.
- web: updating an access group invalidates user queries so effective
  values and inherit hints refresh; changing the group inside the user
  edit dialog degrades inherit hints to generic labels instead of showing
  the old group's values; the invitation form labels null library scope
  as inherit-from-group rather than 'All libraries'.

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

fix(metadata): gate book enrichment on a credible match, and drain the no_match backlog
…e-performance

fix(metadata): make artwork recovery explicit
…connection

feat(watchsync): add per-connection plugin config
…ck-stall

fix(transcodenode): stop force reload blocking playback on the node
…rial-metadata

fix(web): align hero editorial metadata
Scoped API keys: an admin:users key could create or take over an admin
account and log in for an unscoped JWT. Scoped actors may no longer
assign the admin role or change credentials on an existing admin.
Self-service POST /api/v1/api-keys now honours `scopes` instead of
silently dropping them, and GET /api/v1/api-keys/scopes advertises
the catalog for feature detection.

Policy resolution: download_transcode_allowed was the one column whose
old default was false, so the migration froze every account as an
explicit deny; it now maps false to inherit, NoGroupPolicy matches the
old default, and Down restores the correct default. Legacy negative
caps map to inherit instead of unlimited. Ungrouped users no longer
query the group store, home sections and item filters fail closed on
a policy error like /libraries, the legacy metadata gate resolves
inherited libraries, and the requests service no longer has a dead
group-only fallback.

Web admin: inherit hints follow the selected group on both tabs and on
the create form, Override no longer seeds 0 (= unlimited), an empty
limit box cannot be saved as 0, the effective panel shows the
group-intersected permissions and the audio-transcode row, and the
access-group copy describes inherit/override.

Cleanups: one tri-state decoder, generic clonePtr, shared cap
validator, table-driven user Update, single policy field table in the
web form, orphaned UserTranscodeLimitField removed, unused
OverrideSources removed.

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

Scoped admin API keys, and inherit/override user policy
Corrections:
- downloads-api §4.11: proxy_delivery means the routes are mounted, not that
  requests redirect; artifact relay applies to /downloads/{id}/file only.
- docker.md: give a real pg_dump backup (live data-dir copies are torn),
  restart postgres+silo together, explain long migrations vs the healthcheck,
  mention --migrate-status/--migrate-down-to, state compat ports are on by
  default.
- AGENTS.md: align "Related issue" wording with the PR template; point the
  pre-PR gate at CONTRIBUTING instead of carrying a second list.
- .env.example: leave POSTGRES_PASSWORD commented so the quick-start append
  is the only definition.
- Issue forms: drop the required Adversarial review field; restore the
  fabricated-report block warning in the bug-report intro.
- feature-changelog: add the missing entry for proxy delivery (Silo-Server#607).

Consolidation:
- One pre-submission gate (CONTRIBUTING), one quick start (docker.md), one
  go.work paragraph (DEVELOPMENT), one AI disclosure block wording.
- README drops the duplicated quick start, tag table, doc index, and filler.
- PR template checklist reduced to the two items its sections don't cover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs: streamline project and contribution guidance
…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>
Quick104 and others added 28 commits August 28, 2026 08:42
…anced tiers

Admins deploying Silo were getting lost in 20 settings tabs that showed
every server key at once. This collapses the settings area into nine
intent-oriented tabs (General, Appearance, Security & Access,
Library & Metadata, Playback, Integrations, Notifications,
Compatibility, Infrastructure) and gives every setting a tier:
Essential (always shown), Advanced (one collapsible per section),
or Hidden (no UI; key still saved and readable via the API).

Shared primitives replace the bespoke per-tab patterns: AdvancedSection,
SecretField (one configured/replace/keep control), LimitField
("Unlimited" instead of 0-means-unlimited hints), ProviderCard (uniform
third-party credential cards), and a RestartBadge sourced from
config.RestartRequired via a new GET /admin/settings/restart-keys
endpoint. Log Retention and Theming move onto the shared
useSettingsForm + SaveBar model, and useSettingsForm now guards
beforeunload when dirty.

Navigation: Settings becomes its own sidebar group with the nine tabs
inline, Autoscan lives under Libraries, the command palette (⌘K) mounts
globally with a visible search button, and every old ?tab= id redirects
to the tab that absorbed it. Missing admin defaults are registered
(matching the loader fallbacks so runtime behaviour is unchanged).

Deliberately deferred: renaming the un-namespaced transcode keys to
playback.*, a canonical server.public_url, and deleting legacy
s3.operational_* rows.

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

metadata.cache_images copies provider artwork into the public S3 bucket,
so the tab-merge label "Store artwork on this server" was misleading and
the toggle could be switched on with nowhere to cache to. Restore the
"S3 Image Caching" name, disable the toggle with a link to Infrastructure
when no bucket exists, and reject enabling it server-side
(storage_unavailable) unless a public bucket is live, saved, env-supplied,
or written in the same batch — the last case keeps the setup wizard's
single-batch save working.

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

Replace the boxed-card settings look with the approved "System Settings +
status overview" design. Settings now opens on an overview that reads the
server's state at a glance — a health strip (storage, database, transcoding,
search, email) and one card per section with live values and attention
markers — and each section page has a large title, a status strip, sentence-
case groups, row-based fields with descriptions, restart and default chips,
dirty rails, an inline Advanced row with counts, a floating save pill, and a
single restart banner. The settings rail shows a status dot per section.

Integrations is split into Subtitles & Metadata (provider tiles that expand
in place), Watch sync (Trakt, Simkl), and AI (model tiles + features);
Discord app credentials move back under Notifications. Old ?tab= ids,
including integrations and watch-providers, redirect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first cut of the redesign read as busy: breadcrumb, lede, and status
strip before every control, two-line descriptions on every row, restart
chips on every storage field, dirty rails and default chips, green-tinted
provider tiles, a rail of identical green dots with its own search box,
and an overview of healthy tiles and 33 green facts.

Keep the structure, remove the decoration: titles alone on section pages,
one-line descriptions only where the label is ambiguous, a single "Changes
apply after a restart" line on all-restart groups, neutral provider tiles,
amber-only rail dots, and an overview that shows only what needs attention
plus one summary line per section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rt note per page

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

Co-Authored-By: Claude Fable 5 <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
PR #175 landed as a squash merge, which dropped silo/main from ancestry.
Record silo/main with an ours merge so GitHub upstream tracking stays at 0
behind while preserving the squashed tree on main.

Co-authored-by: Jonah May <JonahMMay@users.noreply.github.com>
@JonahMMay
JonahMMay merged commit d502b92 into main Aug 28, 2026
2 checks passed
@JonahMMay
JonahMMay deleted the cursor/fix-silo-ancestry-d673 branch August 28, 2026 15:01
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