merge: repair silo upstream ancestry after accidental squash - #176
Merged
Conversation
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>
…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>
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 #175 was squash-merged, which dropped
silo/mainfrommain'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 oursmerge ofsilo/mainonto currentmain. This records silo as a parent without changing any files —git diff main..HEADis empty. After merge, upstream tracking should read 0 behind silo again.Merge instructions: use "Create a merge commit". Do not squash.
Validation
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
Checklist