## What Completes the `/user/account` redesign behind the `accountSettingsV2` flag. The earlier commits on this branch built the two-pane shell and converted three panes; this finishes the other five, then polishes the result against the Pencil canvas (`designs/user-account.pen`). **OFF serves the legacy single-column page byte-identically.** See "Rollout" below. ## Panes | Pane | Change | |---|---| | Overview | Tier badge art in the Membership tile (links `/user/membership`); Standing replaces the creator-score figure; username renders its nameplate + badge cosmetics; identity card stacks on mobile | | Profile & Account | `ProfileCard` / `SocialProfileCard` flattened; Account standing shows the exact score; session refresh and delete are pointer rows, grouped | | Preferences | Regrouped to Media playback / Generation / File preferences / Features; image format moved to File preferences; assistant folded into Features | | Content & Browsing | Eye callout; mature-content rows; Topics as chips; hidden tags/users flattened | | Creator | Placement, remix and metric-visibility sections; sticker inventory pointer moved inside Stickers | | Membership & Billing | Subscription / payment methods / payouts flattened; gifts point at `/pricing/gift`; membership row stacks on mobile; empty states when the user has neither a membership nor a Creator Program payout config | | Security & Apps | Sign-in methods, API keys, OAuth apps, connected apps flattened; create buttons on the section heading | | Notifications | Delivery section; per-category icons; more room in an open category; `Other` sorted last | ## Decisions worth a reviewer's attention - **Cards take a `flat` prop rather than being forked.** The legacy page mounts the same components while the flag is alive; two copies of a settings form is how one of them silently loses a field. - **One rule per section.** Eight rows had nine dividers and read as a table. Rows are spaced instead. - **`/user/account/overview` is a new URL.** On mobile the index renders the section *menu*, so an overview reachable only at the index has no way in. `AccountLayout` takes `isIndex` from the route now; inferring it from `section.path` rendered the menu at both URLs. Covered by a test — deleting the alias 404s that URL. - **Standing thresholds moved to `accountStandingFromPoints`** (`strike.schema.ts`). Two surfaces show standing and it derives from active *points*, not the strike count. - **The sticker-inventory pointer survives its host section's bail paths.** It is not gated on placement, so nesting it inside that section would drop it whenever the placement controls cannot render (flag off, or a failed spaces read). - **`BrowsingCategories` switched to chips outright**, including the legacy card, rather than growing a variant prop — one rendering, no fork. - **First use of a Tailwind `has-[…]` variant in this repo** (`SettingRow`, to keep switch rows inline at every width). Tailwind is 3.4.17, so it is supported. - **Billing empty states are `flat`-only.** `SubscriptionCard` and `UserPaymentConfigurationCard` both returned `null` with nothing to show, which left the whole pane blank. They now offer the plans / the Creator Program instead — but only in the flat panes, so the legacy page keeps hiding them and stays byte-identical. Both reuse the metric-visibility upsell, extracted as `UpsellPanel`. ## Verification Typecheck clean; no new lint warnings. Covering suites green: `account-sections` (17), `strike.service` + `process-strikes` (75), the four Account browser suites (24), and the notification suites (79). `SettingsCard.earlyAdopter.browser` needed one assertion updated — it pinned the literal early-adopter copy. Kept its intent (the opt-in must explain itself) and split it into the promise and the caveat rather than loosening it. Walked every pane at 1440px and 390px as a subscribed Creator Program account, and the billing/notification changes on a free account with no Creator Program. ## Not in this PR - `designs/user-account.pen` has uncommitted local changes that predate this work; left alone deliberately. ## Rollout `accountSettingsV2` → Flipt key `account-settings-v2`. `availability: ['mod']` is the STATIC FALLBACK only — it decides nothing while Flipt answers, so it matters solely during a Flipt outage, where mods get the new shell and everyone else keeps the legacy page. The Flipt rollout is the on-switch: `account-settings-v2` is `enabled: false` with no rollouts today, so the page is off for everyone until a segment or threshold rollout is merged in `flipt-state`. Instant rollback = drop that rollout / set the threshold to 0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_013NnY26APwddt5dySmmubkZ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NnY26APwddt5dySmmubkZ
12 KiB
Feature Flag Cleanup Candidates
Audit of src/server/services/feature-flags.service.ts — flags worth removing or rethinking, grouped by confidence.
How a flag gets used (search before deleting)
Before deleting any flag, grep for all of these patterns. The destructure pattern in particular is easy to miss:
| Pattern | Example |
|---|---|
features.X |
if (features.clubs) ... |
| Destructure | const { apiKeys, oauthApps } = useFeatureFlags(); |
ctx.features.X |
server-side tRPC handlers |
ext.flags?.X |
DataGraph node extensions (e.g. wan-graph.ts) |
isFlagProtected('X') |
tRPC router middleware |
'X' literal |
component-config maps (e.g. AppFooter footer items) |
Flipt key (X-name) |
direct isFlipt(...) / evaluateBoolean(FliptFlag.X) calls |
Also check the FliptFlag enum in src/server/flipt/client.ts.
Tier 1 — Truly dead (already removed)
| Flag | Status |
|---|---|
imageIndex |
✅ Removed — zero consumers |
apiKeys |
❌ Restored — gates ApiKeysCard in AccountPanes.tsx:92 and LegacyAccountPage.tsx:62 |
oauthApps |
❌ Restored — gates OAuthAppsCard + ConnectedAppsCard in AccountPanes.tsx:93-94 and LegacyAccountPage.tsx:63-64 |
⚠️ Every account-page flag has two consumers while accountSettingsV2 is alive — the pane
(AccountPanes.tsx) and the fallback (LegacyAccountPage.tsx). One grep hit is not the whole answer.
apiKeys: ['public'] is decorative-only (always-true gate); see Tier 4.
Tier 2 — Dead features (need code rip-out alongside flag deletion)
| Flag | Line | Cleanup scope |
|---|---|---|
clubs |
127 | 40+ consumers — full feature rip-out (router, components, pages) |
createClubs |
128 | Only club.router.ts:48,93; drops with clubs |
coinbasePayments |
171 | availability: [] → already off. Remove dead branches in BuzzPurchase.tsx:555, BuzzPurchaseImproved.tsx:896, coinbase.router.ts |
nowpaymentPayments |
173 | availability: [] → already off; same pattern as coinbase |
paddleAdjustments |
156 | Comment says "temporarily disabled until we change ads provider" — confirm with team before deleting |
Tier 3 — Single-reference flags worth product review
Each has exactly one real consumer; the question is whether the feature itself is still needed.
| Flag | Line | Sole consumer | Product question |
|---|---|---|---|
questions |
90 | pages-old/questions/... | Lives in pages-old/ — strong signal the section is archived. Delete page + flag together |
kinguinIframe |
177 | KinguinCheckout.tsx:85 | Is Kinguin still being used at all? |
annualMemberships |
168 | MembershipPlans.tsx:221 | ['dev'] — ship it or remove |
civitaiLink |
190 | CivitaiLinkProvider.tsx:355, CivitaiLinkPopover.tsx:341 | Keep. Desktop v1.21.0 and node pack v0.6.0 both shipped Sep 2026; ['mod','member'] is the supporter gate, not a rollout leftover |
thirtyDayEarlyAccess |
174 | constants.ts:1708,1718 | Sets early-access duration ceiling to 30 days — likely still meaningful, but verify |
prepaidBuzzTransactions |
187 | PrepaidBuzzTransactions.tsx:81 | Single mod component |
safety |
123 | AppFooter.tsx:36 | Just gates a footer link |
comicSearch |
160 | AutocompleteSearch.tsx:516, pages/search/comics.tsx:28 | Comics search page exists — keep? |
adminTags |
57 | article.controller.ts:37 | ['mod', 'granted'] — could be a plain permission check |
moderateTags |
129 | pages/moderator/tags.tsx:234 | ['granted'] — could be a plain permission check |
Tier 4 — Long-public flags worth promoting (decorative-only)
These have been ['public'] forever with no Flipt key, so the gate always evaluates true. Each features.X consumer can be inlined to true (or just the gate removed). This is mostly a code-tidying pass — there's no risk of behavior change.
canWrite, apiKeys, articles, articleCreate, articleImageScanning, imageGeneration, collections, profileCollections, imageSearch, buzz, cosmeticShop, donationGoals, appTour, privateModels, toolSearch, vault, draftMode, membershipsV2, prepaidMemberships, newsroom, bounties (mostly public), creatorComp, alternateHome, auctions (public), disablePayments, challengePlatform, largerGenerationImages (toggleable but defaulted), air (toggleable but defaulted), assistant (toggleable but defaulted).
⚠️ Before promoting any of these, double-check that ENV overrides via FEATURE_FLAG_X are not expected to flip them off in some deployment.
Open question — only ship truthy flags to the client?
We could trim the client payload by sending only flags whose value is true (and treating missing entries as false). Past attempts broke a few consumers — the candidates are:
- Code that distinguishes
falsefromundefined— e.g.if (features.X === false)orif (features.X !== undefined). - Code that treats
undefinedas a "still loading" signal — relevant if any consumer uses the lazy/async accessor. !features.Xchecks — these continue to work (undefinedis falsy), but subtle bugs are possible if downstream logic later asserts the type.
Audit checklist before attempting again:
- Grep for
features.X === falseandfeatures.X !== undefined - Grep for explicit
Boolean(features.X)/!!features.X(these are fine but signal intent we should preserve) - Audit the
getFeatureFlagsLazyconsumers — theObject.definePropertygetter pattern meansundefinedreads short-circuit throughobj.features, which may have implications for serialization - Check
FeatureAccesstype — currentlyRecord<FeatureFlagKey, boolean>. If we ship a partial, the type needs to becomePartial<Record<FeatureFlagKey, true>>so consumers can't read.Xwithout a truthiness check
Easiest safe step: keep the full payload but normalize all consumers to truthy-only checks (if (features.X) not if (features.X === true)), then ship the partial payload as a follow-up once we're confident nothing relies on the negative case.