feat(skills): tenant selector with its own save, for super admins only - #12
Merged
Conversation
`owner_tenant_id` is a snapshot taken at creation, so a user moved between
tenants leaves their skills behind: the old tenant's admin still manages
them, the new tenant's admin cannot. This adds the control that fixes it,
in the skill editor, visible to super admins only.
It saves on its own button rather than with the form. Reassignment changes
ownership, not content — a published skill is the same skill afterwards, so
routing it through save-and-review would cost production a working skill for
no reason. Backend counterpart is a dedicated `PUT /skills/{id}/tenant`.
The warning panel lists what the operator cannot see from the dropdown: the
old tenant's admins lose access, tenant-shared content becomes readable by
everyone in the new tenant, the department is cleared, and a name collision
in the target tenant will fail. It appears only once the selection actually
differs — a warning that is always on is not a warning.
Small things that came from imagining the failure: the button is disabled
rather than hidden when nothing changed, because a button appearing out of
nowhere reads as "my selection wasn't registered"; it is re-enabled after a
failure, so you can rename and retry; and a failed tenant list shows an
error instead of an empty dropdown, which would read as "there are no other
tenants" rather than "this didn't load".
Backend 409/403 detail text is surfaced verbatim — "target tenant already
has a skill named X, rename it first" tells the user what to do next, which
a generic failure toast does not.
## The proxy layer, and a test for it
The first attempt returned `404` from Next.js. Every `/api/v1/skills/*` path
needs an explicit proxy file under `pages/api/v1/skills/`, and this one was
missing, so the request never reached the backend at all. The 404 looks like
"the backend has no such endpoint" while the backend is fine — it sends
debugging in exactly the wrong direction.
That is the third time recently that adding a path meant forgetting the
layer that has to be updated in lockstep (pnpm-workspace.yaml and
builtin_skills/ both had to be added to the Dockerfile). So the new test does
not just pin this one file: it scans the front-end sources for every
`/api/v1/skills/...` call and checks each one resolves to a proxy file,
understanding both `[id]` and `[...path]` segments. It keys off the requests
the front end actually makes, since those are what 404.
246 passed. tsc unchanged at the 135 baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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.
owner_tenant_idis a snapshot taken at creation, so a user moved betweentenants leaves their skills behind: the old tenant's admin still manages
them, the new tenant's admin cannot. This adds the control that fixes it, in
the skill editor, visible to super admins only.
Pairs with ragent-service #71.
It saves on its own button
Reassignment changes ownership, not content — a published skill is the same
skill afterwards, so routing it through save-and-review would cost production
a working skill for no reason. The backend counterpart is a dedicated
PUT /skills/{id}/tenantthat leaves review status untouched.The warning panel
It lists what the operator cannot see from the dropdown: the old tenant's
admins lose access, tenant-shared content becomes readable by everyone in the
new tenant, the department is cleared, and a name collision in the target
tenant will fail the save.
It appears only once the selection actually differs. A warning that is always
on is not a warning.
Details that came from imagining the failure
button appearing out of nowhere reads as "my selection wasn't registered".
which would read as "there are no other tenants" rather than "this didn't
load".
has a skill named X, rename it first" tells the user what to do next, which
a generic failure toast does not.
The proxy layer, and a test for it
The first attempt returned
404— from Next.js, not the backend. Every/api/v1/skills/*path needs an explicit proxy file underpages/api/v1/skills/, and this one was missing, so the request neverreached the backend at all. The 404 looks like "the backend has no such
endpoint" while the backend is fine, which sends debugging in exactly the
wrong direction.
That is the third time recently that adding a path meant forgetting the layer
which has to be updated in lockstep —
pnpm-workspace.yamlandbuiltin_skills/both had to be added to the Dockerfile, with the samesilent failure mode.
So the new test does not merely pin this one file. It scans the front-end
sources for every
/api/v1/skills/...call and checks each resolves to aproxy file, understanding both
[id]and[...path]segments. It keys offthe requests the front end actually makes, since those are what 404.
Mutation-verified by removing this proxy file and an unrelated one; both go
red, naming the missing path.
246 passed.
tscunchanged at the 135 baseline.🤖 Generated with Claude Code