feat(dashboard/hands): GitHub/Codeberg registry source selector#6142
Conversation
Closes #6137. The hand and skill registry source defaulted to GitHub with no way to switch to Codeberg from the UI — the backend `registry.registry_host` config (added in #6095 / #6103 for the Codeberg-hosted registry) was only reachable by hand-editing config.toml. This adds a compact source selector to the Hands page header, wired through the existing config query/mutation hooks: - Reads `registry.registry_host` via `useFullConfig()`; maps unset/`https://github.com` to GitHub and `https://codeberg.org` to Codeberg. - Writes via `useSetConfigValue({ path: "registry.registry_host", value })` — GitHub clears it to null (the default), Codeberg sets the Codeberg base URL. The mutation already invalidates the config cache, so the control reflects the persisted value. - A pre-existing custom forge URL (set out-of-band in config.toml) is shown as a read-only "Custom (host)" option so it is never silently misrepresented as GitHub/Codeberg; switching to GitHub/Codeberg from there still works. - Surfaces `restart_required` from the set response in the success toast. No backend changes — the field, the GET /api/config and POST /api/config/set routes, and the GitHub-vs-Forgejo URL handling in registry_sync all already exist. i18n: new `hands.registry_source*` keys added to en, zh, uk (locale parity check passes). Verification: dashboard `pnpm typecheck`, `eslint` (0 errors), `i18n-parity`, and `vitest run src/pages/HandsPage.test.tsx` (15/15, incl. 3 new selector tests: default GitHub, switch-to-Codeberg writes the host, switch-back clears to null) all pass.
…mment, remove shadowing aria-label Multi-line JSDoc on RegistrySourceSelector contained issue-number refs and described what the code does — both prohibited by project comment rules. The two-line inline comment for the custom-host option exceeded the one-line-max rule. The aria-label on the <select> was redundant because the <label htmlFor> already supplies the accessible name; keeping both caused aria-label to silently shadow the programmatic label association.
|
Pushed one follow-up commit ( 1. Removed the 6-line JSDoc on 2. Collapsed the two-line inline comment ( 3. Removed the redundant No logic or test changes — the three new selector tests still cover the same cases. Generated by Claude Code |
Addresses review findings on the registry-source selector, all in code this branch introduced. - Explicit `registry_host = "https://github.com"` is no longer masked as the GitHub option. registry_sync treats any `Some(host)` as the Forgejo archive scheme (`/archive/{branch}.tar.gz`, prefix `{repo}/`), which does not match GitHub's tarball layout — so an explicit github.com is a broken value distinct from the `null` default. It now surfaces as `Custom (...)`, and picking GitHub clears it to `null` to fix it. Previously the dropdown showed "GitHub" as if healthy and `next === current` made the GitHub option a no-op, so the bad value was unfixable from the UI. - The control is now disabled during the post-write refetch (`isFetching`) and on a config-read error (`isError`), not only on `isLoading`/`isPending`. This stops the controlled `<select>` snapping back to the stale value before the invalidated query resolves, and prevents a stray click from overwriting a real config with `null` when the current value could not be read. - The read-only custom `<option>` is no longer `disabled`: a controlled `<select>` whose value points at a disabled option is undefined across browsers (and warns in React). Re-selecting custom is a no-op via the existing `handleChange` guard. - A failed live reload (`reload_error` in the set response) now toasts an error instead of a misleading success — the value persisted but is not in effect, mirroring ConfigPage. Tests: +5 (explicit-github→custom + clears to null, disabled on error, disabled during refetch, reload_error→error toast, restart_required toast). The test harness now uses a stable addToast spy and captures the mutation's onSuccess/onError so the toast branches are actually exercised. typecheck, eslint (0 errors), and vitest (20/20) pass. Not changed: a mixed-case/trailing-slash custom Codeberg URL (e.g. "https://Codeberg.org/") reads correctly as Codeberg but is not rewritten to the canonical host — that is a backend host-normalization concern in registry_sync, out of scope for this selector.
…b default Root-cause fix for the issue the dashboard selector was papering over: registry_urls treated ANY Some(host) as a Forgejo/Codeberg forge, so an explicit registry_host = "https://github.com" derived the Forgejo archive scheme (`/archive/{branch}.tar.gz`, prefix `{repo}/`) instead of GitHub's (`/archive/refs/heads/{branch}.tar.gz`, prefix `{repo}-{branch}/`). GitHub's tarball extracts to `{repo}-{branch}/`, so the prefix never matched and the sync silently failed to find the files. registry_urls now trims trailing slashes, drops an empty host string, and folds an explicit github.com host (any case) back to the GitHub branch, so it can never reach the Forgejo path with an incompatible scheme. Codeberg and other forges are unchanged. With the backend tolerant, the dashboard selector now shows an explicit github.com host as GitHub (it is equivalent to the unset default) rather than masking it as a broken "custom" value — the two layers now agree that github.com == GitHub. Tests: registry_urls_explicit_github_host_uses_github_scheme covers "https://github.com", a trailing slash, mixed case, and the empty string. The HandsPage selector test asserts github.com reads as GitHub. cargo test -p librefang-runtime registry_urls (4/4) and clippy -D warnings pass; dashboard typecheck/eslint/vitest (20/20) pass.
Summary
Closes #6137.
The Hands ("开发团队") catalogue is pulled from a registry that defaulted to GitHub, with no way to switch to Codeberg from the dashboard — the backend
registry.registry_hostconfig (landed in #6095 / #6103 for the Codeberg-hosted registry) could only be changed by hand-editingconfig.toml. This surfaces it as a UI control, and hardens the backend host handling it relies on.Change
A compact source selector in the Hands page header, wired entirely through the existing config data-layer hooks:
useFullConfig()→registry.registry_host. Unset or an explicitgithub.comshows GitHub (the default);codeberg.org(any case / trailing slash) shows Codeberg; anything else shows Custom (host).useSetConfigValue({ path: "registry.registry_host", value }). GitHub clears it tonull; Codeberg sets the Codeberg base URL. The mutation already invalidates the config cache, so the control reflects the persisted value.<select>snapping back to the stale value), errored (prevents a stray click overwriting the real config withnull), or a write is in flight.reload_error) toasts an error, not a misleading success;restart_requiredtoasts the restart variant.Backend root-cause fix
registry_sync::registry_urlstreated anySome(host)as a Forgejo/Codeberg forge, so an explicitregistry_host = "https://github.com"derived the Forgejo archive scheme (/archive/{branch}.tar.gz, prefix{repo}/) — which does not match GitHub's tarball layout (/archive/refs/heads/{branch}.tar.gz, prefix{repo}-{branch}/), so the sync silently failed. It now trims trailing slashes, drops an empty host, and folds an explicitgithub.com(any case) back to the GitHub branch. This is what lets the UI honestly treatgithub.comas GitHub instead of masking it as a broken value.Conventions
Follows the dashboard rules: API access only through
src/lib/queries/src/lib/mutationshooks, theme tokens (no hex),lucide-reacticon, accessible name via<label htmlFor>, TypeScript strict (noany), loading/empty/error states handled.Files
crates/librefang-runtime/src/registry_sync.rs— github.com host folding + test.crates/librefang-api/dashboard/src/pages/HandsPage.tsx—RegistrySourceSelector.crates/librefang-api/dashboard/src/pages/HandsPage.test.tsx— config-hook mocks + selector tests.crates/librefang-api/dashboard/src/locales/{en,zh,uk}.json—hands.registry_source*strings.Verification
cargo test -p librefang-runtime registry_urls(4/4, incl. the explicit-github-host case) andcargo clippy -p librefang-runtime -D warningspass.pnpm typecheck,eslint(0 errors),i18n-parity(all locales), andvitest run src/pages/HandsPage.test.tsx(20/20, incl. default GitHub, switch-to-Codeberg writes the host, switch-back clears to null, explicit github.com reads as GitHub, disabled on error / during refetch, reload_error → error toast, restart_required toast) pass.Out of scope
The issue screenshot also shows the "开发团队" hand's own GitHub-specific settings (
ghCLI, GitHub token). Making that hand operate against Codeberg is a change to the hand'sHAND.toml, which lives in the external (Codeberg-hosted) registry, not this repo — so it is not addressable here.