Skip to content

feat(config): Operate · Configuration + Feature flags (Batch 4 / T403+T404, PR-14) - #33

Merged
ghbvf merged 8 commits into
developfrom
013-b4-config-flags
Jun 2, 2026
Merged

ghbvf merged 8 commits into
developfrom
013-b4-config-flags

Conversation

@ghbvf

@ghbvf ghbvf commented Jun 2, 2026

Copy link
Copy Markdown
Owner

Summary

Batch 4 / T403 + T404 — the Operate · Configuration and Feature flags pages, second of the two PRs for #15. Builds the @gocell/config cell (config entries + flags + useFlag).

Stacked on PR #32 (@gocell/audit). Base is 012-b4-audit; retarget to develop once #32 merges. The config cell shares no files with audit — only the additive apps/web/router + core/i18n assembly, already reconciled by rebasing onto #32's tip.

Configuration (T403) — config.entries store

  • api/config.ts — list/get/write/update(CAS)/publish/rollback/delete against /api/v1/config/. CAS via expectedVersion.
  • views/ConfigView.vue + ConfigEditDrawer.vue + ConfirmDialog.vue — versioned table, stage→publish flow, rollback, sensitive-value redaction guard.
  • lib/configValidation.ts — key/value validation + isRedactedPlaceholder write-back guard.

Feature flags (T404) — config.flags store + useFlag

  • api/flags.ts — list/get/create/update(PUT, full)/toggle(kill switch)/evaluate/delete against /api/v1/flags/.
  • views/FlagsView.vue + FlagFormModal.vue + RolloutSlider.vue — flag cards, bool toggle, rollout %, kill switch (danger confirm).
  • composables/useFlag.ts + flags/registry.ts — FLAG_KEYS as const → FlagKey literal union. useFlag(key: FlagKey) makes a hardcoded flag name a compile error (AI-robust Hard: violation is inexpressible). Reverse self-check via @ts-expect-error (unknown key rejected, known key passes). Reads the flags-list cache (not per-flag /evaluate); missing key → false (fail-safe).

Backend gaps (frontend-first per #15 — graceful degradation, BRs filed)

Gap Frontend behaviour BR
flag variant (create/update/evaluate carry no variant value; type is a free string) bool flags fully built; type read-only; explicit "variant configuration coming soon" placeholder (no internal IDs surfaced to users) BR-007
config version-history list endpoint (rollback needs a target version but none can be enumerated) rollback dialog takes a number input (1…current-1) BR-008

Packages touched

  • @gocell/config (new cell): api / stores / composables / lib / views / components / README / exports.
  • @gocell/core: i18n config.entries.* + flags.{list,form}.* blocks (zh-CN + en-US).
  • apps/web: /config (PDP read:config) + /flags (PDP read:flag) routes + @gocell/config dependency.
  • packages/contracts/: not modified — consumed read-only.

Test plan

  • pnpm -F @gocell/config test:coverage — 252 tests pass, 97.25% statements (≥80%)
  • pnpm -w typecheck — clean (incl. useFlag @ts-expect-error reverse self-check + en-US satisfies MessageSchema)
  • pnpm -w lint — 0 issues
  • pnpm -w test --run — 62 files / 950 tests pass (incl. new /config + /flags router assertions)

Refs #15

ghbvf and others added 6 commits June 2, 2026 09:59
…k (CAS)

Implements T403 config-entries slice (Batch 4):
- src/api/config.ts: listConfig/getConfig/writeConfig/updateConfig/deleteConfig/publishConfig/rollbackConfig, CONFIG_URL, ConfigEntry type (codegen-derived)
- src/lib/configValidation.ts: validateConfigKey/validateConfigValue/isRedactedPlaceholder (sensitive write guard)
- src/stores/useConfigStore.ts: Pinia setup store (id: config.entries), CAS, mutating flag, sensitive guard, race-safe mutations
- src/stores/index.ts: C2 append point for useFlagsStore
- src/components/ConfirmDialog.vue: config-local copy (candidate for @gocell/core promotion)
- src/components/ConfigEditDrawer.vue: create/edit form with stage warning strip, sensitive blank-on-edit
- src/views/ConfigView.vue: page-head + table + three states + publish/rollback/delete confirm dialogs
- 133 tests, 97.92% stmt / 85.96% fn / 87.86% branch coverage

Degraded decisions: no version-history endpoint (BR-008 pending) → rollback manual version input; no draft state in contract → stage semantics via subtitle; sensitive redaction guard prevents "******" write-back.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d-key registry

Implements the flags slice (T404) for @gocell/config:

- src/api/flags.ts: FLAGS_URL, FeatureFlag type, listFlags/getFlag/createFlag/updateFlag/toggleFlag/evaluateFlag/deleteFlag (CAS, percent-encode, 409 re-throw)
- src/flags/registry.ts: FLAG_KEYS as const + FlagKey literal union (Hard-档 AI-robust — unknown literals are compile-time errors)
- src/composables/useFlag.ts: useFlag(key: FlagKey) → readonly ComputedRef<boolean> consuming useFlagsStore cache; fail-safe false when key not found
- src/stores/useFlagsStore.ts: defineStore('config.flags') — state/filteredFlags/isEnabled; fetchList/loadMore; create/update/toggle/remove (re-throw, CAS)
- src/components/RolloutSlider.vue: native <input type="range"> 0–100, aria-valuenow/min/max, clamp, keyboard-operable
- src/components/FlagFormModal.vue: ModalShell-based create/edit (key, description, enabled, rolloutPercentage, type read-only display, variant coming-soon placeholder)
- src/views/FlagsView.vue: v1-flag cards, v1-switch, v1-flag-bar rollout progress (CSS var data-driven), kill switch danger confirm, delete confirm, <Can> guards, 3-state (loading/empty/error)
- Appended useFlagsStore to src/stores/index.ts and flag exports to src/index.ts
- README updated with flags domain section

Degradation decisions:
- variant config UI not implemented; type field read-only + placeholder (BR-007 pending: flag variant schema)
- useFlag uses list cache, not /evaluate (admin endpoint, not suitable for high-frequency)

Tests: 252 pass (119 new), coverage 97.25% stmts, typecheck clean, lint 0 issues

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…g cell

Expose @gocell/config to apps/web; declare exports (./stores ./composables
./views/config ./views/flags); mirror access vitest/coverage config.

Refs #15
…atch 4 PR-14)

- apps/web: register /config (PDP read:config) and /flags (PDP read:flag) as
  AppShellLayout children; lazy-load @gocell/config view subpaths.
- core i18n: add config.entries.* and flags.{list,form}.* blocks (zh-CN + en-US).
- router spec: assert /config and /flags resolve nested with auth + PDP meta.

Refs #15

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- BR-006: audit row hash chain + actor/result metadata (audit page degraded
  until exposed; verifyChain ready).
- BR-007: feature flag variant definition + evaluate value (bool ships now;
  variant placeholder).
- BR-008: config version-history list endpoint (rollback uses manual version
  input until then).

Refs #15
…ot Cx1-Cx2

F1: sensitive aria-label now bound via :aria-label + t() (was bare string literal)
F2: toggle wrapped in <Can write/flag>; aria-label moved to <input>; @change replaced
    with onToggleChange — enabled→disabled path: preventDefault + askKill (checkbox
    reverted visually); disabled→enabled path: immediate store.toggle call
F3: registry.spec FlagKey assignment removes `as FlagKey` cast — direct assignment
    from Object.values(FLAG_KEYS) is the real compile-time check
F4: FlagFormModal + ConfigEditDrawer focus-visible outline restored to
    2px solid var(--accent) + offset 2px (was outline:none)
F5+F9: RolloutSlider — hardcoded id removed, replaced with useId() fallback;
    ariaLabel prop removed; accessible name comes from parent <label :for>
    association; FlagFormModal wires rolloutSliderId via useId()
F6: FlagsView border-radius 4px → var(--r-sm), 999px → var(--r-pill)
F7: useFlagsStore.isEnabled O(n)→O(1) via flagMap computed (Map keyed by flag.key)
F8: useFlagsStore.flags + useConfigStore.entries changed to shallowRef
F10: flags.list.confirm.cancel added to en-US + zh-CN; FlagsView kill/delete
    dialogs now pass cancel-key="flags.list.confirm.cancel" explicitly
F11: api/config.ts + api/flags.ts use HttpConfigListV1Response /
    HttpConfigFlagsListV1Response directly; local ConfigListPage / FlagListPage
    interfaces removed
F12: ConfirmDialog gains default <slot /> between message and error; rollback
    version input moved from standalone v-if div into rollback ConfirmDialog slot
    (input now inside modal panel, accessible within focus trap)
F13: useFlag / FLAG_KEYS / FlagKey removed from @gocell/config root index.ts;
    exposed only via @gocell/config/composables
F14: tsconfig.base.json paths: @gocell/config sub-paths added (composables,
    stores, views/config, views/flags) matching package.json#exports

Tests: 259 passing (config), 957 passing (workspace)
Coverage: 97% stmts / 87.74% branches (≥80% threshold met)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ghbvf
ghbvf changed the base branch from 012-b4-audit to develop June 2, 2026 11:33
ghbvf and others added 2 commits June 2, 2026 19:34
…itle

- FlagsView: a failed *enable* toggle no longer opens the kill-switch confirm
  dialog (which prompted "disable this flag?" after an enable error). It now
  reverts the optimistic native checkbox flip and surfaces the error inline via
  a dedicated toggleErrorKey alert. Adds a regression test.
- i18n: zh-CN flags.list.title was the English placeholder "Feature flags";
  translated to "功能开关" to match nav.flags and the零硬编中英文 rule.

config 260 tests + core i18n 29 tests pass; typecheck + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ghbvf

ghbvf commented Jun 2, 2026

Copy link
Copy Markdown
Owner Author

六维度 Review(包边界 / Vue 模板 / TS / 包架构 / a11y / 性能 / 设计一致性)

整体质量积极:包边界无违规(无裸 axios、无横向业务 cell import、HTTP 经 @gocell/request、Can/useDecision 来自 @gocell/core、@gocell/core/components 是合规 export 子路径);useFlag 的 FLAG_KEYS as const typed-key 是 Hard 档 AI-robust 设计且配反向自检测试;tsconfig.base.json 新增 4 条 path 映射与 package.json#exports 完全对应;BR-007/008 后端 gap 诚实降级标注。config 包 260 测试 + core i18n 29 测试全绿。

已修复(commit 82c74dc)

  1. [Vue / a11y / 产品] FlagsView 启用失败误弹 kill 对话框 — onToggleChange 的 enable 分支失败时 killFlag.value = flag,会打开 kill-switch 确认对话框(标题「Kill switch / 关闭此 flag?」)。用户本是「启用」flag,失败后却看到「关闭确认」是误导。改为:失败时回滚乐观的原生勾选 + 走独立 toggleErrorKey 内联 alert 展示错误,不再打开 kill 对话框。补回归测试。
  2. [i18n] zh-CN flags.list.title 英文占位 — 值为 'Feature flags',与同 namespace 下中文 subtitle 不一致、违反「零硬编中英文」。改为 '功能开关'(对齐 nav.flags)。

评估后不改(登记说明 + 理由)

  • mutating 守卫不一致(reviewer P0):publish 用 Promise.reject,其余 void mutation 用 if (mutating.value) return。这是由返回类型决定的合理差异——publish 返回 PublishSnapshot 无法 silently 返回 void,故必须 reject;void mutation 静默 no-op 是正当的 debounce 守卫。且各浮层(modal/drawer/dialog)提交时 :busy 禁用自身按钮 + ModalShell 焦点陷阱遮挡背景,跨动作并发竞态实际不可达;最坏情况(两个 fire-and-forget toggle 并发)也是自纠正(refreshLoaded 后 :checked 回正),无数据损坏、无虚假成功。判定低危,不 churn 8 处。
  • FlagFormModal aria-invalid 与 v-if 条件不一致(reviewer P1):误报。keyError 仅在 submitted=true 后(第97行)被赋值,keyError 非空 ⟹ submitted 必为 true,故 aria-invalid="!!keyError" 与 v-if="keyError && submitted" 恒一致,无 AT 噪声。
  • ConfirmDialog 静态 title-id/description-id(reviewer P0):ModalShell 用 v-if="open" 保证同刻仅一个实例渲染,当前互斥使用下 id 不重复,无活跃 bug。提升到 @gocell/core 时再用 useId() 重构(已知 promote-to-core 债)。
  • vue-router 未使用依赖(P2):确认 config/src 0 引用,属依赖冗余;但移除需 pnpm install 重写 lockfile(frozen-lockfile CI 联动),本 slice 不动,登记后续清理。
  • 变体 badge 每卡可见 / padding 28·32 token 缺口 / RolloutSlider resolvedId 响应性 / flags__action 28px / ConfigEditDrawer description-id:分别为产品取舍、token 系统级缺口(lint 已放行)、inputId 实为静态非问题、与设计紧凑密度一致、a11y nice-to-have。均不 block,登记备案。

结论:✅ 可合并(reviewer 结论本身为「不 block」)。

@ghbvf
ghbvf merged commit 991e8bf into develop Jun 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant