feat: add deployment setting to disable the Codernauts game - #27662
Conversation
Adds a hide_codernauts runtime setting persisted in site_configs and exposed through the appearance API. A new toggle at the bottom of the Deployment > Appearance page controls it, and the user dropdown skips the Codernauts menu item when it is set. The setting works on all licenses: the default appearance fetcher reads it from the database too, so no license entitlement is required.
Docs previewCheck off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here. |
| VALUES ( | ||
| 'hide_codernauts', | ||
| CASE | ||
| WHEN sqlc.arg(hide)::bool THEN 'true' |
There was a problem hiding this comment.
| WHEN sqlc.arg(hide)::bool THEN 'true' | |
| WHEN @hide::bool THEN 'true' |
There was a problem hiding this comment.
Done in adeb84d. 🤖 (Coder Agents on behalf of @bartekgatzcoder)
| ) | ||
| ON CONFLICT (key) DO UPDATE | ||
| SET value = CASE | ||
| WHEN sqlc.arg(hide)::bool THEN 'true' |
There was a problem hiding this comment.
| WHEN sqlc.arg(hide)::bool THEN 'true' | |
| WHEN @hide::bool THEN 'true' |
There was a problem hiding this comment.
Done in adeb84d. 🤖 (Coder Agents on behalf of @bartekgatzcoder)
| -- GetHideCodernauts returns whether the Codernauts game link is hidden | ||
| -- from the user dropdown menu. Defaults to false when unset. |
There was a problem hiding this comment.
| -- GetHideCodernauts returns whether the Codernauts game link is hidden | |
| -- from the user dropdown menu. Defaults to false when unset. |
yap
There was a problem hiding this comment.
Removed in adeb84d. 🤖 (Coder Agents on behalf of @bartekgatzcoder)
| ServiceBanner BannerConfig `json:"service_banner"` | ||
| AnnouncementBanners []BannerConfig `json:"announcement_banners"` | ||
| SupportLinks []LinkConfig `json:"support_links,omitempty"` | ||
| // HideCodernauts hides the Codernauts game link in the user dropdown menu. |
There was a problem hiding this comment.
| // HideCodernauts hides the Codernauts game link in the user dropdown menu. |
yap
There was a problem hiding this comment.
Removed in adeb84d. 🤖 (Coder Agents on behalf of @bartekgatzcoder)
| // Deprecated: ServiceBanner has been replaced by AnnouncementBanners. | ||
| ServiceBanner BannerConfig `json:"service_banner"` | ||
| AnnouncementBanners []BannerConfig `json:"announcement_banners"` | ||
| // HideCodernauts hides the Codernauts game link in the user dropdown menu. |
There was a problem hiding this comment.
| // HideCodernauts hides the Codernauts game link in the user dropdown menu. |
yap
There was a problem hiding this comment.
Removed in adeb84d. 🤖 (Coder Agents on behalf of @bartekgatzcoder)
| // database may be nil when no store is available, in which case | ||
| // runtime settings fall back to their zero values. |
There was a problem hiding this comment.
why tolerate nil?
this is classic "yap", words for the sake of words without actually saying what the recipient wants to hear. I do not need to be told that nil means a store in unavailable, I need to know why we would tolerate that. what benefit to we get out of letting it be optional instead of just requiring it?
There was a problem hiding this comment.
Fair point, there was no real benefit. adeb84d drops the nil tolerance: the fetcher now requires a store, and site.New passes opts.Database. 🤖 (Coder Agents on behalf of @bartekgatzcoder)
| export const CodernautsShown: Story = { | ||
| play: async ({ canvasElement, step }) => { | ||
| await step("shows the Codernauts link by default", async () => { | ||
| await openDropdown(canvasElement); | ||
| expect( | ||
| screen.getByRole("menuitem", { name: "Codernauts" }), | ||
| ).toBeInTheDocument(); | ||
| }); | ||
| }, | ||
| }; |
There was a problem hiding this comment.
we don't need a specific story for this. it's already captured in every other story.
There was a problem hiding this comment.
Removed the CodernautsShown story in adeb84d. 🤖 (Coder Agents on behalf of @bartekgatzcoder)
| // No database is available here; coderd replaces this fetcher | ||
| // with a database-backed one during startup. | ||
| f := appearance.NewDefaultFetcher(nil, opts.DocsURL) |
There was a problem hiding this comment.
I'm pretty sure it's just opts.Database. we definitely have a database handle somewhere in here, because we use it for fetching user data/token state/etc.
Line 75 in 2af08bb
There was a problem hiding this comment.
Correct, it is. adeb84d passes opts.Database and removes the comment. 🤖 (Coder Agents on behalf of @bartekgatzcoder)
Use the @arg sqlc shorthand, drop comments that restate the code, require a database store in the default appearance fetcher by passing site.Options.Database, and remove the redundant CodernautsShown story.
…s-toggle # Conflicts: # site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx # site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx
Rename hide_codernauts to codernauts_enabled across the API, database queries, and frontend. The toggle now reads "Codernauts game" with a neutral description, defaults to on, and switching it off disables the game.
Covers the upgrade path: with no stored site config the game is enabled, disabling works without a license, and the stored value survives switching to the licensed appearance fetcher.
| func (q *querier) GetCodernautsEnabled(ctx context.Context) (bool, error) { | ||
| // No authz checks | ||
| return q.db.GetCodernautsEnabled(ctx) | ||
| } |
There was a problem hiding this comment.
| func (q *querier) GetCodernautsEnabled(ctx context.Context) (bool, error) { | |
| // No authz checks | |
| return q.db.GetCodernautsEnabled(ctx) | |
| } | |
| func (q *querier) GetCodernautsEnabled(ctx context.Context) (bool, error) { | |
| return q.db.GetCodernautsEnabled(ctx) | |
| } |
There was a problem hiding this comment.
Applied in 51a9b27. 🤖 (Coder Agents on behalf of @bartekgatzcoder)
| ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) | ||
| defer cancel() |
There was a problem hiding this comment.
I can't remember off the top of my head but is this definitely the intended way to create a context for a test in coder/coder?
Regardless it should be moved to just before first use (so right before basicUserClient.Appearance(ctx). Otherwise coderdentest.New and coderdtest.CreateAnotherUser start ticking down the timer and we're more likely to get a flake.
There was a problem hiding this comment.
Good catch. Switched to testutil.Context (the repo convention, with automatic cancel on cleanup) and moved it to right before the first use so setup no longer eats into the timeout. Done in 51a9b27. 🤖 (Coder Agents on behalf of @bartekgatzcoder)
Drop the no-authz comment from GetCodernautsEnabled and use testutil.Context created right before first use in the Codernauts test to avoid eating into the timeout during setup.
…s-toggle # Conflicts: # site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx # site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx
Adds a
codernauts_enabledruntime setting (default: enabled) that controls whether the Codernauts game link appears in the user dropdown menu. A toggle at the bottom of the Deployment > Appearance page controls it, and the value persists insite_configs, so it survives restarts and upgrades without any CLI flags or environment variables.The setting works on all licenses:
GET /api/v2/appearancereports it from both the enterprise and the default (AGPL) appearance fetchers, andPUT /api/v2/appearancepersists it gated only by the deployment-config RBAC permission, not by an entitlement. On the Appearance page the toggle renders outside the Premium paywall introduced in #27948; the other appearance fields keep their existing licensing behavior.Note: a pure AGPL-only build of coderd does not register the
/api/v2/appearanceroutes at all, so the toggle cannot be saved there. The standardcoder serverbinary is unaffected regardless of license state.Implementation notes
coderd/database/queries/siteconfig.sql: newGetCodernautsEnabled/UpsertCodernautsEnabledqueries backed by acodernauts_enabledkey insite_configs; defaults totruewhen unset (no migration needed).coderd/database/dbauthz: read has no authz checks (matching other appearance reads); write requiresResourceDeploymentConfigupdate. Coverage added todbauthz_test.go.codersdk:codernauts_enabledadded toAppearanceConfigandUpdateAppearanceConfig.coderd/appearance: the default fetcher takes adatabase.Storeand reads the setting from the database so unlicensed deployments serve it too.enterprise/coderd/appearance.go: fetches the value inFetchand persists it inputAppearance.codernauts_enabledthreaded fromuseDashboard().appearancethrough Navbar toUserDropdownContent, which renders the menu item only when enabled. Storybook stories withplayfunctions cover toggling (entitled and not) and the dropdown hiding the link when disabled.🤖 This PR was generated by Coder Agents on behalf of @bartekgatzcoder.