{"owner":"redpanda-data","repo":"console","hasSkills":true,"totalSkillsCount":89,"totalTokensCount":66504,"categories":["command-prompt","copilot-instructions","root-instruction","claude-rule","subagent-persona","anthropic-skill","cursor-rule"],"hasMcp":false,"mcpConfig":null,"found":[".claude/commands/changelog/update.md",".claude/commands/fix-snyk-vulnerabilities.md",".github/copilot-instructions.md","AGENTS.md","CLAUDE.md","backend/AGENTS.md","backend/CLAUDE.md","frontend/.claude/agents/playwright-test-generator.md","frontend/.claude/agents/playwright-test-healer.md","frontend/.claude/agents/playwright-test-planner.md","frontend/.claude/agents/redpanda-logo-fetcher.md","frontend/.claude/rules/test-unit-vs-integration.md","frontend/.claude/skills/api-patterns/SKILL.md","frontend/.claude/skills/api-patterns/rules/api-invalidate-cache.md","frontend/.claude/skills/api-patterns/rules/api-toast-errors.md","frontend/.claude/skills/api-patterns/rules/protobuf-no-edit.md","frontend/.claude/skills/api-patterns/rules/use-connect-query.md","frontend/.claude/skills/code-standards/SKILL.md","frontend/.claude/skills/code-standards/rules/async-await-promises.md","frontend/.claude/skills/code-standards/rules/no-legacy.md","frontend/.claude/skills/code-standards/rules/react-functional-only.md","frontend/.claude/skills/e2e-tester/SKILL.md","frontend/.claude/skills/e2e-tester/references/container-setup.md","frontend/.claude/skills/e2e-tester/references/failure-analysis.md","frontend/.claude/skills/e2e-tester/references/test-patterns.md","frontend/.claude/skills/form-refactorer/SKILL.md","frontend/.claude/skills/form-refactorer/references/common-patterns.md","frontend/.claude/skills/react-best-practices/SKILL.md","frontend/.claude/skills/react-best-practices/rules/advanced-event-handler-refs.md","frontend/.claude/skills/react-best-practices/rules/advanced-use-latest.md","frontend/.claude/skills/react-best-practices/rules/async-defer-await.md","frontend/.claude/skills/react-best-practices/rules/async-dependencies.md","frontend/.claude/skills/react-best-practices/rules/async-suspense-boundaries.md","frontend/.claude/skills/react-best-practices/rules/bundle-barrel-imports.md","frontend/.claude/skills/react-best-practices/rules/bundle-code-splitting.md","frontend/.claude/skills/react-best-practices/rules/bundle-preload.md","frontend/.claude/skills/react-best-practices/rules/client-event-listeners.md","frontend/.claude/skills/react-best-practices/rules/client-passive-event-listeners.md","frontend/.claude/skills/react-best-practices/rules/js-batch-dom-css.md","frontend/.claude/skills/react-best-practices/rules/js-caching-patterns.md","frontend/.claude/skills/react-best-practices/rules/js-index-maps.md","frontend/.claude/skills/react-best-practices/rules/js-length-check-first.md","frontend/.claude/skills/react-best-practices/rules/js-tosorted-immutable.md","frontend/.claude/skills/react-best-practices/rules/rendering-activity.md","frontend/.claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md","frontend/.claude/skills/react-best-practices/rules/rendering-conditional-render.md","frontend/.claude/skills/react-best-practices/rules/rendering-content-visibility.md","frontend/.claude/skills/react-best-practices/rules/rendering-hoist-jsx.md","frontend/.claude/skills/react-best-practices/rules/rendering-svg-precision.md","frontend/.claude/skills/react-best-practices/rules/rerender-defer-reads.md","frontend/.claude/skills/react-best-practices/rules/rerender-dependencies.md","frontend/.claude/skills/react-best-practices/rules/rerender-derived-state.md","frontend/.claude/skills/react-best-practices/rules/rerender-functional-setstate.md","frontend/.claude/skills/react-best-practices/rules/rerender-lazy-state-init.md","frontend/.claude/skills/react-best-practices/rules/rerender-memo.md","frontend/.claude/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md","frontend/.claude/skills/react-best-practices/rules/rerender-transitions.md","frontend/.claude/skills/react-best-practices/rules/use-effect.md","frontend/.claude/skills/security-scan/SKILL.md","frontend/.claude/skills/security-scan/rules/_sections.md","frontend/.claude/skills/security-scan/rules/_template.md","frontend/.claude/skills/security-scan/rules/vuln-direct-deps.md","frontend/.claude/skills/security-scan/rules/vuln-transitive-deps.md","frontend/.claude/skills/state-management/SKILL.md","frontend/.claude/skills/state-management/rules/use-react-query-for-server.md","frontend/.claude/skills/state-management/rules/use-zustand-persist.md","frontend/.claude/skills/state-management/rules/use-zustand-selectors.md","frontend/.claude/skills/tanstack-router-migration/SKILL.md","frontend/.claude/skills/tanstack-router-migration/references/build-configuration.md","frontend/.claude/skills/tanstack-router-migration/references/incremental-migration.md","frontend/.claude/skills/tanstack-router-migration/references/migration-patterns.md","frontend/.claude/skills/tanstack-router-migration/references/route-templates.md","frontend/.claude/skills/testing/SKILL.md","frontend/.claude/skills/testing/rules/mock-transport.md","frontend/.claude/skills/testing/rules/no-ui-rendering-tests.md","frontend/.claude/skills/testing/rules/test-mock-patterns.md","frontend/.claude/skills/testing/rules/test-unit-vs-integration.md","frontend/.claude/skills/testing/rules/test-zustand-stores.md","frontend/.claude/skills/ui-development/SKILL.md","frontend/.claude/skills/ui-development/rules/icon-system.md","frontend/.claude/skills/ui-development/rules/registry-upgrade.md","frontend/.claude/skills/ui-development/rules/style-no-margin-on-registry.md","frontend/.claude/skills/ui-development/rules/style-use-tokens.md","frontend/.claude/skills/ui-development/rules/style-use-variants.md","frontend/.claude/skills/ui-development/rules/use-ui-registry.md","frontend/.claude/skills/web-design-guidelines/SKILL.md","frontend/AGENTS.md","frontend/CLAUDE.md",".cursorrules (kubestellar-console.mdc)"],"skills":{".claude/commands/changelog/update.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=.claude%2Fcommands%2Fchangelog%2Fupdate.md -->",".claude/commands/fix-snyk-vulnerabilities.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=.claude%2Fcommands%2Ffix-snyk-vulnerabilities.md -->",".github/copilot-instructions.md":"# Copilot instructions: Redpanda Console\n\nGo backend (`backend/`) + React 19 SPA (`frontend/`) sharing a protobuf contract (`proto/`).\nTask drives builds from the repository root.\n\n## Where the rules live\n\nArea rules are **not** repeated here. They live in `AGENTS.md` files and the nearest one to a\nchanged file wins:\n\n| Path | Governing file |\n|---|---|\n| `backend/**` | `backend/AGENTS.md` |\n| `frontend/**` | `frontend/AGENTS.md` |\n| `proto/**`, root config, `taskfiles/**` | `AGENTS.md` |\n| commits, PR description, review conduct | this file |\n\nWhen a review comment enforces an area rule, name the file it came from so the author can\nread the rule in context.\n\n## Commit messages\n\nFormat is `area[/detail]: short description`.\n\nexample:\n```\nbackend: resolve schema IDs in the topic's registry context\n\nBefore this change the message viewer always looked schemas up in the\ndefault context, so a topic bound to a named context returned a decode\nerror. Resolution now takes the topic's context, which also removes the\nspecial case in the sample handler.\n```\n\n- Subject: 50 characters or fewer, lowercase after the prefix, no trailing period.\n- Common areas: `backend`, `frontend`, `proto`, `topics`, `chore`, `deps`.\n- Body: wrap at 72 columns. Explain **why**, not what: the state being moved away from, why\n  it is no longer wanted, and how the new state addresses that. Then any non-obvious\n  technical detail.\n- A commit message must stand alone. No \"in the previous commit\", no references to meetings\n  or threads. That context goes in the PR description.\n- One logical change per commit, and every commit must build on its own so `git bisect`\n  works.\n\nParts of the history use Conventional Commits (`feat(frontend): ...`). `area:` is the\ndocumented standard in `CONTRIBUTING.md` and the form to use for new work.\n\n## Pull requests\n\n- Title follows the same `area: description` convention.\n- The description carries the high-level narrative that individual commits must not depend\n  on, plus examples of what's being added.\n- Fold review fixups back into the commits they modify before merge. Do not leave\n  \"address review feedback\" commits in the final history.\n- `git rebase --keep-base` keeps the diff for reviewers minimal on a force-push.\n- Call out explicitly when a change touches `backend/pkg/api/hooks.go` or `proto/`. Those are\n  the contract surfaces `console-enterprise` builds on, and a signature change there breaks\n  it. (`backend/pkg/api/hooks/` is a stale duplicate that nothing imports.)\n\n## Comments\n\nRepo-wide: keep comments to a minimum. Less is more. Full rules in\n[backend/AGENTS.md](../backend/AGENTS.md#comments).\n\n- Comment the **why**, never the what. A non-obvious constraint, a protocol quirk, a\n  deliberate deviation. Not `// increment the offset` above `offset++`.\n- Doc comments on exported Go identifiers are mandatory (`revive` runs with all rules on) and\n  are the exception to this, not a license to annotate everything.\n- No banner comments, no commented-out code, no comments that exist to describe what the diff\n  changed. That belongs in the commit message.\n- `TODO`/`FIXME` must name a ticket or an owner.\n- Prefer a better name or a small extracted function over a paragraph explaining the code.\n\nWhen reviewing, flag added comments that restate the code and suggest deleting them. This is\nthe most common form of bloat in AI-assisted pull requests.\n\n## Review priorities\n\nIn order:\n\n1. **Correctness.** Wrong behavior, unhandled errors, swallowed errors, nil dereferences,\n   races, resource leaks, missing `ctx` propagation.\n2. **The area rules** in the relevant `AGENTS.md`, especially the \"Hard rules\" section of\n   `backend/AGENTS.md` and the \"Critical Rules\" of `frontend/AGENTS.md`.\n3. **Contract and compatibility.** Proto changes that are not wire-compatible, hook signature\n   changes, API version placement, generated output not regenerated.\n4. **Tests.** New behavior needs a test. Backend prefers table-driven with `t.Run`; frontend\n   splits `.test.ts` (unit, node) from `.test.tsx` (integration, happy-dom).\n5. **Noise.** Comments that restate the code, commented-out code, and dead scaffolding. See\n   Comments above.\n\n## Do not flag\n\n- Anything under a generated or vendored path: `backend/pkg/protogen/`,\n  `frontend/src/protogen/`, `frontend/src/routeTree.gen.ts`,\n  `frontend/src/components/redpanda-ui/`, `build/`. Changes there are either generated output\n  or upstream registry content.\n- Formatting and style nits already enforced by tooling. `golangci-lint` 2.10 (37 linters,\n  `revive` with all rules on, `sloglint`, `gci`) and Biome/Ultracite gate those in CI, and a\n  duplicate review comment adds noise without adding information.\n- Line length, import order, or trailing whitespace. The formatters own those.\n\n## What CI enforces\n\n| Workflow | Gate |\n|---|---|\n| `backend-lint-test.yml` | `golangci-lint` + unit and integration tests |\n| `frontend-verify.yml` | Biome/Ultracite (with a dirty-tree check), `ultracite doctor`, type check, build, tests |\n| `buf.yml` | proto lint, format, and breaking-change detection |\n| `proto-generate.yml` | fails if generated proto output is not committed |\n| `frontend-react-doctor.yml` | React Doctor on frontend changes |\n| `frontend-ui-audit.yml` | advisory only: registry drift, off-token colors, ad-hoc utility classes |\n| `repository-dispatch.yml` | triggers the `console-enterprise` build after verify |\n\nPrefer pointing at the gate that will catch an issue over restating what the gate already\nsays.\n","AGENTS.md":"# Redpanda Console\n\nGo backend (`backend/`) + React 19 single-page app (`frontend/`) sharing a protobuf API\ncontract (`proto/`). Builds and tooling are driven by [Task](https://taskfile.dev/) from the\nrepository root.\n\n## Which rules apply where\n\nRead the nearest file to what you are changing. The nested files own their area; this file\nonly routes.\n\n| You are changing | Read |\n|---|---|\n| `backend/**` | [backend/AGENTS.md](backend/AGENTS.md) |\n| `frontend/**` | [frontend/AGENTS.md](frontend/AGENTS.md) |\n| `proto/**` | this file, plus `buf.yaml` and `taskfiles/proto.yaml` |\n| root config, `taskfiles/**`, `.github/**` | this file |\n| commit messages, PR description, review | [.github/copilot-instructions.md](.github/copilot-instructions.md) and [CONTRIBUTING.md](CONTRIBUTING.md) |\n\n## Commands\n\nBackend and proto commands run **from the repository root** via `task`. The `./taskw` wrapper\npins the Go toolchain and helper binaries under `build/`.\n\n```bash\ntask backend:fmt            # format\ntask backend:lint           # golangci-lint\ntask backend:test-unit      # unit tests\ntask backend:test-integration   # unit + integration (needs Docker)\ntask backend:verify         # lint + generate + integration tests\ntask proto:lint             # buf lint\ntask proto:generate         # regenerate backend + frontend protogen\n```\n\nFrontend commands run **from `frontend/`** via `bun run <script>`.\n\n## Never edit (generated or vendored)\n\n- `backend/pkg/protogen/` and `frontend/src/protogen/` (regenerate with `task proto:generate`)\n- `frontend/src/routeTree.gen.ts` (TanStack Router)\n- `frontend/src/components/redpanda-ui/` (UI Registry, sourced from `redpanda-data/ui-registry`)\n- `build/` (pinned toolchain)\n\n## Enterprise boundary\n\nThis repository is the **OSS foundation**. `console-enterprise` imports\n`github.com/redpanda-data/console/backend` and extends it through the hooks system in\n`backend/pkg/api/hooks.go`, which lets it add interceptors, change routes, and replace service\nimplementations.\n\nFeatures that live behind those hooks (authentication, RBAC, secret management, shadow links,\npipelines, debug bundles) **cannot be exercised from this repository alone**. To test an OSS\nchange against enterprise, add a replace directive in `console-enterprise/backend/go.mod`:\n\n```go\nreplace github.com/redpanda-data/console/backend => /path/to/console/backend\n```\n\n## Proto changes\n\n`proto/` is published to the Buf Schema Registry as `buf.build/redpandadata/dataplane`. A\nchange here is an API contract change:\n\n1. Edit the `.proto` file.\n2. `task proto:lint && task proto:format`\n3. `task proto:generate` and commit the generated output. `proto-generate.yml` fails the PR if\n   generated code is out of date.\n4. Breaking-change detection runs in CI. Some alpha packages are exempt; check the `ignore`\n   list in `buf.yaml` before assuming yours is. Everything else is wire-compatible-only.\n","CLAUDE.md":"@AGENTS.md\n","backend/AGENTS.md":"# Redpanda Console Backend\n\nGo HTTP API server for managing and debugging Kafka/Redpanda workloads. Serves a legacy REST\nAPI under `/api` and a Connect RPC API re-exposed as JSON through gRPC-Gateway under\nversioned paths.\n\nModule: `github.com/redpanda-data/console/backend`. See [../AGENTS.md](../AGENTS.md) for\nrepo-wide rules.\n\n**How to use this file.** It tells you the rules you cannot infer and where to look for the\nrest. It is not a specification. Before relying on a specific, read the source: the linter\nconfig for lint rules, the Taskfile for commands, `routes.go` for what is wired, and the\npackage you are editing for its own patterns. When this file and the code disagree, the code\nwins and this file needs a fix.\n\n## Hard rules\n\nNon-obvious and enforced. Everything else the linter will tell you.\n\n**Errors.** Construct Connect errors through `apierrors`\n([pkg/api/connect/errors/](pkg/api/connect/errors/)), never `connect.NewError` directly, or\nyou lose the error details the helper always attaches. That package also holds converters for\nKafka, Schema Registry, and Redpanda Admin API errors; check for one before writing your own.\nLegacy REST handlers use a different model, `*rest.Error` with `rest.SendRESTError`.\n\n**Logging.** `log/slog` only, and `sloglint` is strict:\n\n- No global logger. Take a `*slog.Logger` as a constructor parameter and store it.\n- Attributes only: `slog.String(\"topic_name\", name)`, never loose key/value pairs.\n- snake_case keys, lowercase messages, and `\"error\"` rather than `\"err\"`.\n- Sub-loggers via `loggerpkg.Named(parent, \"topic_service\")`.\n\n**Doc comments.** `revive` runs with `enable-all-rules: true`, so every exported identifier\nneeds a doc comment.\n\n**License header.** Every new `.go` file starts with the BSL block:\n\n```go\n// Copyright 2026 Redpanda Data, Inc.\n//\n// Use of this software is governed by the Business Source License\n// included in the file licenses/BSL.md\n//\n// As of the Change Date specified in that file, in accordance with\n// the Business Source License, use of this software will be governed\n// by the Apache License, Version 2.0\n```\n\n**Imports.** Three blocks: standard library, third-party, then\n`github.com/redpanda-data/console/backend`. `task backend:fmt` sorts them.\n\n**Generated code.** Never hand-edit `pkg/protogen/`. Run `task proto:generate` from the\nrepository root.\n\nComplexity limits are enforced too, but you will hit those as lint failures rather than\nneeding to plan for them. The full linter set is in [.golangci.yaml](.golangci.yaml).\n\n## Comments\n\nKeep comments to a minimum. Less is more.\n\n- Doc comments on exported identifiers are required, as above. One line is enough.\n- Do not restate the code. `// increment the offset` above `offset++` is noise.\n- No banner comments (`// --- helpers ---`), no labelling the obvious.\n- Comment the **why**: a non-obvious constraint, a protocol quirk, a deliberate deviation, a\n  reason an ordering matters. Link the ticket or upstream issue if there is one.\n- Prefer a better name or a small extracted function over a paragraph of explanation.\n- No commented-out code. `TODO`/`FIXME` must name a ticket or an owner.\n\nDo not add comments to code you touched just to show what changed. That goes in the commit\nmessage.\n\n## Commands\n\nFrom the **repository root**. `task --list` has the rest.\n\n```bash\ntask backend:fmt                # format\ntask backend:lint               # golangci-lint\ntask backend:test-unit          # unit tests\ntask backend:test-integration   # unit + integration, needs Docker\ntask backend:verify             # lint, generate, integration tests\ntask backend:generate           # go generate, proto, format\n```\n\n`CLI_ARGS` is forwarded: `task backend:test-integration -- ./pkg/api/...`.\n\nTo invoke `go` directly, run it **from `backend/`**, since the commands are module-relative:\n\n```bash\ngo test ./pkg/serde/... -run TestJsonSerde\ngo test --tags=integration ./pkg/api/connect/integration/... -run TestSuite/TestTopicV1\n```\n\nRun the server from `backend/`, with a local cluster from `docs/local/`\n(`docker compose up -d`):\n\n```bash\nKAFKA_BROKERS=localhost:9092 SERVEFRONTEND=false SERVER_LISTENPORT=9090 go run cmd/api/main.go\n```\n\n**Before pushing:** `task backend:fmt && task backend:lint && task backend:test-unit`, then\n`task backend:test-integration` if you touched `pkg/api/` or `pkg/console/`.\n\n## Where things are\n\nOnly `cmd/` and `pkg/`. No `internal/`.\n\n| Path | Holds |\n|---|---|\n| `cmd/api/` | the single binary |\n| `pkg/api/` | HTTP and gRPC layer. `api.go` wires dependencies, `routes.go` mounts everything, `hooks.go` is the enterprise extension contract |\n| `pkg/api/connect/service/<domain>/<version>/` | Connect service implementations |\n| `pkg/console/` | core business logic behind the `Servicer` interface |\n| `pkg/factory/` | Kafka, Redpanda Admin, and Schema Registry client factories, consumed as interfaces |\n| `pkg/serde/` | auto-detecting serialization for Kafka messages |\n| `pkg/config/` | koanf config, one file per sub-struct |\n| `pkg/logger/` | slog setup with functional options |\n| `pkg/testutil/` | testcontainers helpers; image tags come from `test-images.json` at the repo root |\n| `pkg/protogen/` | generated, do not edit |\n\n`routes.go` is the map. It shows which services exist, which API versions are mounted, and\nwhich are OSS placeholders that enterprise replaces. Read it rather than assuming.\n\n## Conventions\n\n- Wrap errors with context: `fmt.Errorf(\"failed to describe topic: %w\", err)`. Compare with\n  `errors.Is`/`errors.As`, never `==` or string matching.\n- `ctx context.Context` is the first parameter, and never stored in a struct.\n- Accept interfaces, return concrete types. Assert conformance at package level:\n  `var _ dataplanev1connect.TopicServiceHandler = (*Service)(nil)`.\n- Constructors are `NewX(cfg, logger, collaborators...)`, dropping any the service does not\n  need. No setters.\n- A service package splits by role: `service.go` for the RPCs, `mapper.go` for proto\n  conversion, `defaulter.go` for request defaults, `util.go` for error translation. The\n  package is named for the domain even inside a `v1/` directory, so imports are aliased\n  (`topicsvcv1`).\n- `pkg/console` puts one exported operation per file, named in snake_case after the operation.\n- Config sub-structs implement `SetDefaults`, `RegisterFlags`, and `Validate` as needed. Do\n  not add an empty method to complete the set. Read `LoadConfig` in\n  [pkg/config/config.go](pkg/config/config.go) before changing precedence; the order of\n  defaults, flags, file, and env is load-bearing and not what you would guess.\n- Wiring is manual, no DI framework.\n\n## Enterprise boundary\n\nThis is the OSS foundation. `console-enterprise` imports it and extends it through the `Hooks`\ncontract in [pkg/api/hooks.go](pkg/api/hooks.go), which lets it add interceptors, replace OSS\nservice implementations, register additional services, and supply license and feature-flag\ninformation.\n\nChanging a hook signature breaks console-enterprise. Call it out in the PR description.\n\nThere is a stale duplicate of these declarations in `pkg/api/hooks/`. Nothing imports it;\ndo not add to it.\n\n## Testing\n\n- Table-driven is the house pattern: a slice of cases with a `name` and a `validateFn`\n  closure, run through `t.Run(tc.name, ...)`. See\n  [topic/v1/mapper_test.go](pkg/api/connect/service/topic/v1/mapper_test.go).\n- testify: `require` where a failure makes later assertions meaningless, `assert` for value\n  comparisons. Both in one test is normal.\n- Tests live in the same package as the code (`package topic`, not `topic_test`).\n- Unit tests carry no build tag. Integration tests carry `//go:build integration` and the\n  `_integration_test.go` suffix, except in `pkg/api/connect/integration/` where the tag alone\n  marks them.\n- Integration suites use testcontainers-go and testify/suite. See\n  [integration/api_suite_test.go](pkg/api/connect/integration/api_suite_test.go).\n- Mocks are gomock, from a `//go:generate mockgen` directive next to the interface.\n  Regenerate with `task backend:generate`.\n","backend/CLAUDE.md":"@AGENTS.md\n","frontend/.claude/agents/playwright-test-generator.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fagents%2Fplaywright-test-generator.md -->","frontend/.claude/agents/playwright-test-healer.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fagents%2Fplaywright-test-healer.md -->","frontend/.claude/agents/playwright-test-planner.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fagents%2Fplaywright-test-planner.md -->","frontend/.claude/agents/redpanda-logo-fetcher.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fagents%2Fredpanda-logo-fetcher.md -->","frontend/.claude/rules/test-unit-vs-integration.md":"---\npaths:\n  - \"src/**/*.test.{ts,tsx}\"\n---\n\n# Test classification\n\nClassify what the test exercises before choosing its filename:\n\n- Pure logic/mocked module contracts: `.test.ts`, Bun (`bun run test:unit`).\n- React, DOM, hooks, state/transport integration: `.test.tsx`, Rstest + Happy DOM.\n- Configuration/subprocess integration: `.integration.test.ts`, Rstest.\n- Native browser and full running-app journeys: `.spec.ts`, Rstest + Playwright.\n\nSee [test guide](../../tests/README.md) for commands and fixtures. Keep full-stack journeys on Testcontainers; mocked bundle tests do not replace them.\n","frontend/.claude/skills/api-patterns/SKILL.md":"---\nname: api-patterns\ndescription: Connect Query patterns for API calls. Use when working with mutations, queries, or data fetching.\n---\n\n# API Patterns\n\nMake API calls with Connect Query and handle responses properly.\n\n## Activation Conditions\n\n- Making API calls\n- Using Connect Query hooks\n- Cache invalidation\n- Mutations and optimistic updates\n- Toast notifications for errors\n\n## Quick Reference\n\n| Action | Rule |\n|--------|------|\n| Fetch data | `use-connect-query.md` |\n| After mutation | `api-invalidate-cache.md` |\n| Handle errors | `api-toast-errors.md` (use `formatToastErrorMessage` in onError) |\n| Protobuf files | `protobuf-no-edit.md` |\n\n## Key Locations\n\n| Location | Purpose |\n|----------|---------|\n| `/src/react-query/` | Connect Query hooks |\n| `/src/protogen/` | Generated protos (DO NOT EDIT) |\n\nRegenerate protos: `task proto:generate` (from repo root)\n\n## Rules\n\nSee `rules/` directory for detailed guidance on queries, mutations, cache invalidation, and error handling.\n","frontend/.claude/skills/api-patterns/rules/api-invalidate-cache.md":"---\ntitle: Invalidate Cache After Mutations\nimpact: HIGH\nimpactDescription: Stale cache after mutations causes UI to show outdated data\ntags: cache, invalidation, mutation, react-query\n---\n\n# Invalidate Cache After Mutations (HIGH)\n\n## Explanation\n\nAfter mutations (create, update, delete), invalidate related queries to ensure the UI reflects the latest server state. Without invalidation, users see stale data until they manually refresh.\n\n## Incorrect\n\n```tsx\n// No invalidation after mutation\nconst mutation = useMutation(updateCluster, {\n  onSuccess: () => {\n    toast.success('Cluster updated');\n    // UI still shows old data!\n  },\n});\n```\n\n```tsx\n// Invalidating wrong query key\nconst mutation = useMutation(updateCluster, {\n  onSuccess: () => {\n    queryClient.invalidateQueries({ queryKey: ['clusters'] }); // Wrong key\n  },\n});\n```\n\n## Correct\n\n```tsx\nimport { useMutation } from '@connectrpc/connect-query';\nimport { useQueryClient } from '@tanstack/react-query';\n\nconst queryClient = useQueryClient();\n\nconst mutation = useMutation(updateCluster, {\n  onSuccess: () => {\n    // Invalidate specific query\n    queryClient.invalidateQueries({\n      queryKey: ['getCluster', clusterId],\n    });\n\n    // Or invalidate all cluster queries\n    queryClient.invalidateQueries({\n      queryKey: ['getClusters'],\n    });\n\n    toast.success('Cluster updated');\n  },\n});\n```\n\n## Optimistic Updates\n\nFor instant UI feedback:\n\n```tsx\nconst mutation = useMutation(updateCluster, {\n  onMutate: async (newData) => {\n    // Cancel outgoing refetches\n    await queryClient.cancelQueries({ queryKey: ['getCluster', id] });\n\n    // Snapshot previous value\n    const previous = queryClient.getQueryData(['getCluster', id]);\n\n    // Optimistically update\n    queryClient.setQueryData(['getCluster', id], newData);\n\n    return { previous };\n  },\n  onError: (err, newData, context) => {\n    // Rollback on error\n    queryClient.setQueryData(['getCluster', id], context?.previous);\n  },\n  onSettled: () => {\n    // Always refetch after mutation settles\n    queryClient.invalidateQueries({ queryKey: ['getCluster', id] });\n  },\n});\n```\n\n## Reference\n\n- [Query Invalidation](https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation)\n- [Optimistic Updates](https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates)\n","frontend/.claude/skills/api-patterns/rules/api-toast-errors.md":"---\ntitle: Toast Error Messages\nimpact: HIGH\nimpactDescription: Consistent error messages help users understand and recover from failures\ntags: error-handling, toast, ux, mutations\n---\n\n# Toast Error Messages (HIGH)\n\n## Explanation\n\nUse `formatToastErrorMessage` utility for consistent, user-friendly error messages in mutation `onError` handlers. This provides a standard format that includes the action, entity, and error details.\n\n## Incorrect\n\n```tsx\n// Generic error message\nconst mutation = useMutation(deleteCluster, {\n  onError: () => {\n    toast.error('Something went wrong');\n  },\n});\n\n// Raw error object\nconst mutation = useMutation(deleteCluster, {\n  onError: (error) => {\n    toast.error(error.message); // May be technical/unhelpful\n  },\n});\n\n// No error handling\nconst mutation = useMutation(deleteCluster, {\n  onSuccess: () => toast.success('Deleted'),\n  // Missing onError!\n});\n```\n\n## Correct\n\n```tsx\nimport { formatToastErrorMessage } from 'utils/toast.utils';\n\nconst mutation = useMutation(deleteCluster, {\n  onSuccess: () => {\n    toast.success('Cluster deleted');\n  },\n  onError: (error) => {\n    toast.error(formatToastErrorMessage({\n      error: {\n        reason: error.message,\n        internalCode: error.code,\n      },\n      action: 'delete',\n      entity: 'cluster',\n    }));\n  },\n});\n\n// Output: \"Failed to delete cluster due to: reason (code: 123)\"\n```\n\n## Standard Actions\n\n| Action | Use For |\n|--------|---------|\n| `create` | POST / create operations |\n| `update` | PUT / PATCH operations |\n| `delete` | DELETE operations |\n| `fetch` | GET operations (rare) |\n\n## Error Boundaries for Render Errors\n\n```tsx\nimport * as Sentry from '@sentry/react';\n\n<Sentry.ErrorBoundary fallback={<ErrorFallback />}>\n  <ClusterDashboard />\n</Sentry.ErrorBoundary>\n```\n\n## Reference\n\n- `src/utils/toast.utils.ts` - Toast utility implementation\n","frontend/.claude/skills/api-patterns/rules/protobuf-no-edit.md":"---\ntitle: Protobuf No Edit\nimpact: CRITICAL\nimpactDescription: Never edit files in protogen/ - they are auto-generated\ntags: protobuf, generated, code-gen\n---\n\n## Protobuf No Edit\n\n**Impact:** CRITICAL - Never edit files in protogen/ - they are auto-generated\n\nThe `/src/protogen/` directory contains auto-generated TypeScript from protobuf schemas. Any manual edits will be lost on regeneration and may cause type mismatches.\n\n**Incorrect:**\n```typescript\n// WRONG: Editing generated file\n// src/protogen/user_pb.ts\nexport interface User {\n  id: string;\n  name: string;\n  customField: string; // WRONG: Manual addition\n}\n```\n\n**Correct:**\n```typescript\n// CORRECT: Extend types outside protogen\n// src/types/user.ts\nimport { User as ProtoUser } from \"protogen/user_pb\";\n\nexport interface ExtendedUser extends ProtoUser {\n  customField: string;\n}\n\n// CORRECT: Transform data in hooks/utils\n// src/hooks/useUser.ts\nconst { data } = useQuery(getUser, { id });\n\nconst extendedUser = useMemo(() => ({\n  ...data,\n  displayName: `${data?.firstName} ${data?.lastName}`,\n}), [data]);\n```\n\n## Regenerating Protos\n\nWhen proto schemas change upstream:\n\n```bash\n# From repository root (not frontend/)\ntask proto:generate\n```\n\n## Finding Generated Files\n\n```\nsrc/protogen/\n├── *_pb.ts           # Message types\n├── *_connect.ts      # Service definitions\n└── *_connectquery.ts # Connect Query hooks\n```\n\n## Common Imports\n\n```typescript\n// Message types\nimport { User, CreateUserRequest } from \"protogen/user_pb\";\n\n// Service methods\nimport { UserService } from \"protogen/user_connect\";\n\n// Query hooks\nimport { getUser, listUsers, createUser } from \"protogen/user-UserService_connectquery\";\n```\n","frontend/.claude/skills/api-patterns/rules/use-connect-query.md":"---\ntitle: Use Connect Query\nimpact: CRITICAL\nimpactDescription: Consistent API patterns enable caching, refetching, and error handling\ntags: connect-query, react-query, api, grpc\n---\n\n# Use Connect Query (CRITICAL)\n\n## Explanation\n\nAlways use Connect Query hooks (`useQuery`, `useMutation`) for API calls. These integrate with React Query for caching, background refetching, and optimistic updates. Never use raw fetch or manual state management for server data.\n\n## Incorrect\n\n```tsx\n// Manual fetch with useState\nfunction ClusterList() {\n  const [clusters, setClusters] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    fetch('/api/clusters')\n      .then(res => res.json())\n      .then(data => {\n        setClusters(data);\n        setLoading(false);\n      });\n  }, []);\n}\n```\n\n```tsx\n// Missing enabled flag\nconst { data } = useQuery(\n  getClusters,\n  { organizationId: undefined } // Will error!\n);\n```\n\n## Correct\n\n```tsx\nimport { useQuery } from '@connectrpc/connect-query';\nimport { getClusters } from 'proto/frontend/controlplane/v1/cluster-ClusterService_connectquery';\n\nfunction ClusterList({ organizationId }: Props) {\n  const {\n    data: clusters,\n    isLoading,\n    error,\n    refetch,\n  } = useQuery(\n    getClusters,\n    { organizationId },\n    {\n      staleTime: 1000 * 60 * 5, // 5 minutes\n      enabled: !!organizationId, // Only fetch when ID exists\n    }\n  );\n\n  if (isLoading) return <Skeleton />;\n  if (error) return <ErrorDisplay error={error} />;\n\n  return <List items={clusters} />;\n}\n```\n\n## Common Options\n\n| Option | Purpose |\n|--------|---------|\n| `enabled` | Conditional fetching |\n| `staleTime` | Cache duration before refetch |\n| `refetchInterval` | Polling interval |\n| `refetchOnWindowFocus` | Refetch when tab focused |\n\n## Reference\n\n- [Connect Query Docs](https://connectrpc.com/docs/web/query/getting-started)\n- [TanStack Query Options](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery)\n- See also: [use-react-query-for-server](../../state-management/rules/use-react-query-for-server.md) — when to use React Query vs Zustand\n","frontend/.claude/skills/code-standards/SKILL.md":"---\nname: code-standards\ndescription: TypeScript, React, and JavaScript best practices enforced by Ultracite/Biome.\n---\n\n# Code Standards\n\nWrite code that is accessible, performant, type-safe, and maintainable.\n\n## Activation Conditions\n\n- Linting errors need fixing\n- Code formatting issues\n- Code review feedback\n- Questions about style guidelines\n\n## Quick Reference\n\n| Action | Rule |\n|--------|------|\n| Write components | `react-functional-only.md` |\n| Async code | `async-await-promises.md` |\n| Avoid legacy libs | `no-legacy.md` |\n\n`any` types and top-level regex are enforced by Biome (`noExplicitAny`, `useTopLevelRegex`).\n\n## Commands\n\n```bash\nbun x ultracite fix     # Format and fix\nbun x ultracite check   # Check for issues\nbun x ultracite doctor  # Diagnose setup\n```\n\n## Quick Fix\n\nMost issues are auto-fixed:\n\n```bash\nbun x ultracite fix\n```\n\n## Console Logging\n\n- **Never** use `console.log`, `console.debug`, or `console.info` in production code\n- Only use `console.error` and `console.warn` for actionable errors\n- For debug logging, wrap in `if (IsDev) { ... }`\n- Don't use `biome-ignore` to suppress console warnings - remove the logs instead\n\n## Rules\n\nSee `rules/` directory for detailed guidance.\n","frontend/.claude/skills/code-standards/rules/async-await-promises.md":"---\ntitle: Async/Await Over Promise Chains\nimpact: HIGH\nimpactDescription: async/await is more readable and makes error handling explicit\ntags: async, await, promises, javascript\n---\n\n# Async/Await Over Promise Chains (HIGH)\n\n## Explanation\n\nUse `async/await` instead of `.then()` chains. Async/await is more readable, makes error handling explicit, and avoids callback hell. Never leave promises floating without handling.\n\n## Incorrect\n\n```typescript\n// Promise chains\nfunction loadData() {\n  fetchUser()\n    .then(user => fetchPosts(user.id))\n    .then(posts => processPosts(posts))\n    .catch(error => handleError(error));\n}\n\n// Floating promise (no await)\nfunction save() {\n  saveToDatabase(data); // Promise returned but not awaited!\n}\n\n// Async in Promise executor\nnew Promise(async (resolve) => {\n  const data = await fetch(url);\n  resolve(data);\n});\n\n// Mixing patterns\nasync function mixed() {\n  return fetch(url).then(res => res.json());\n}\n```\n\n## Correct\n\n```typescript\n// async/await\nasync function loadData() {\n  try {\n    const user = await fetchUser();\n    const posts = await fetchPosts(user.id);\n    return processPosts(posts);\n  } catch (error) {\n    handleError(error);\n  }\n}\n\n// Always await or return promises\nasync function save() {\n  await saveToDatabase(data);\n}\n\n// Parallel execution when needed\nasync function loadAll() {\n  const [users, posts] = await Promise.all([\n    fetchUsers(),\n    fetchPosts(),\n  ]);\n  return { users, posts };\n}\n\n// Consistent async/await\nasync function fetchJson(url: string) {\n  const response = await fetch(url);\n  return response.json();\n}\n```\n\n## Error Handling\n\n```typescript\n// Explicit try/catch\nasync function riskyOperation() {\n  try {\n    const result = await mightFail();\n    return result;\n  } catch (error) {\n    // Log and rethrow or handle\n    console.error('Operation failed:', error);\n    throw error;\n  }\n}\n\n// Error boundaries for React\n<ErrorBoundary fallback={<ErrorFallback />}>\n  <AsyncComponent />\n</ErrorBoundary>\n```\n\n## Reference\n\n- [MDN async/await](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Promises)\n","frontend/.claude/skills/code-standards/rules/no-legacy.md":"---\ntitle: No Legacy Patterns\nimpact: CRITICAL\nimpactDescription: Never use legacy patterns - they block migration efforts\ntags: legacy, migration, mobx, yup\n---\n\n## No Legacy Patterns\n\n**Impact:** CRITICAL - Never use legacy patterns - they block migration efforts\n\nThe codebase is actively migrating away from legacy libraries. Using them in new code blocks migration and creates tech debt.\n\n## Prohibited Patterns\n\n| Legacy | Modern Alternative |\n|--------|-------------------|\n| MobX, decorators | Zustand for global state |\n| Class components | Functional components + hooks |\n| Yup validation | Zod |\n| `any` type | `unknown` or specific types |\n| Jest | Rstest |\n\n## Common Violations\n\n**MobX:**\n```typescript\n// WRONG\nimport { makeAutoObservable, observable, action } from \"mobx\";\nimport { observer } from \"mobx-react-lite\";\n\n@observer\nclass Store {\n  @observable users = [];\n}\n```\n\n```typescript\n// CORRECT\nimport { create } from \"zustand\";\n\nconst useStore = create<State>((set) => ({\n  users: [],\n  addUser: (user) => set((s) => ({ users: [...s.users, user] })),\n}));\n```\n\n**Class Components:**\n```tsx\n// WRONG\nclass UserProfile extends React.Component<Props, State> {\n  componentDidMount() { /* ... */ }\n  render() { return <div />; }\n}\n```\n\n```tsx\n// CORRECT\nconst UserProfile = ({ userId }: Props) => {\n  const { data } = useQuery(getUser, { id: userId });\n  return <Card>{data?.name}</Card>;\n};\n```\n\n## Migration Guidance\n\nWhen modifying files with legacy patterns:\n1. **Don't add** new legacy imports\n2. **Consider migrating** the component if changes are significant\n3. **Keep changes minimal** if just fixing bugs\n4. **New features** must use modern patterns\n\n### MobX → React State: Check Side-Effects\n\nWhen converting observables to React state, automatic behaviors must become explicit:\n- MobX observable arrays auto-update UI when mutated\n- React state requires explicit `setState()` calls\n- Clearing, resetting, or initializing state needs manual handling\n\n**Checklist**: List all observable mutations → add equivalent `setState` calls.\n\n## Legacy Locations\n\n- MobX stores: `src/state/` (do not add new files)\n\n## See Also\n\n- [use-ui-registry](../../ui-development/rules/use-ui-registry.md) — Registry component usage and installation\n","frontend/.claude/skills/code-standards/rules/react-functional-only.md":"---\ntitle: Functional Components Only\nimpact: HIGH\nimpactDescription: Class components are deprecated patterns that make code harder to test and compose\ntags: react, components, functional, hooks\n---\n\n# Functional Components Only (HIGH)\n\n## Explanation\n\nAlways use functional components with hooks. Class components are a legacy pattern that makes code harder to test, compose, and optimize. Functional components are the modern React standard.\n\n## Incorrect\n\n```tsx\n// Class component\nclass UserProfile extends React.Component<Props, State> {\n  state = { loading: true };\n\n  componentDidMount() {\n    this.fetchUser();\n  }\n\n  render() {\n    return <div>{this.state.loading ? 'Loading...' : this.props.name}</div>;\n  }\n}\n\n// Nested component definition (causes remounting)\nfunction Parent() {\n  function Child() {\n    return <div>Child</div>;\n  }\n  return <Child />; // Child remounts every render!\n}\n\n// Conditional hooks\nfunction Component({ enabled }) {\n  if (enabled) {\n    const [state, setState] = useState(); // Rules of Hooks violation\n  }\n}\n```\n\n## Correct\n\n```tsx\n// Functional component\nexport function UserProfile({ userId }: Props) {\n  const [loading, setLoading] = useState(true);\n  const [user, setUser] = useState<User | null>(null);\n\n  useEffect(() => {\n    fetchUser(userId).then(data => {\n      setUser(data);\n      setLoading(false);\n    });\n  }, [userId]);\n\n  if (loading) return <Skeleton />;\n\n  return <div>{user?.name}</div>;\n}\n\n// Sibling components defined outside\nfunction Parent() {\n  return <Child />;\n}\n\nfunction Child() {\n  return <div>Child</div>;\n}\n\n// Hooks always at top level\nfunction Component({ enabled }: Props) {\n  const [state, setState] = useState<string | null>(null);\n\n  if (!enabled) return null;\n\n  return <div>{state}</div>;\n}\n```\n\n## Component Patterns\n\n```tsx\n// Named export (preferred)\nexport function FeatureComponent() { }\n\n// Colocated sub-components\nexport function Feature() {\n  return (\n    <div>\n      <FeatureHeader />\n      <FeatureContent />\n    </div>\n  );\n}\n\n// Keep sub-components in same file if small\nfunction FeatureHeader() { return <h1>Title</h1>; }\nfunction FeatureContent() { return <p>Content</p>; }\n```\n\n## Reference\n\n- [React Hooks](https://react.dev/reference/react/hooks)\n- [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks)\n","frontend/.claude/skills/e2e-tester/SKILL.md":"---\nname: e2e-tester\ndescription: \"Write and run Rstest/Playwright E2E tests for Redpanda Console using testcontainers. Analyzes test failures, adds missing testids, and improves test stability. Use when user requests E2E tests, Playwright tests, integration tests, test failures, missing testids, or mentions 'test workflow', 'browser testing', 'end-to-end', or 'testcontainers'.\"\nallowed-tools: Read, Write, Edit, Bash, Glob, Grep, Task, mcp__ide__getDiagnostics\n---\n\n# E2E testing with Rstest, Playwright and Testcontainers\n\nWrite end-to-end tests using Playwright against a full Redpanda Console stack running in Docker containers via testcontainers.\n\n## When to Use This Skill\n\n- Testing 2+ step user journeys (login -> action -> verify)\n- Multi-page workflows\n- Browser automation with Playwright\n\n**NOT for:** Component unit tests -> use [testing](../testing/SKILL.md)\n\n## Critical Rules\n\n**ALWAYS:**\n- Run `bun run build` before running E2E tests (frontend assets required)\n- Use `testcontainers` API for container management (never manual `docker` commands in tests)\n- Use `page.getByRole()` and `page.getByLabel()` selectors (avoid CSS selectors)\n- Add `data-testid` attributes when semantic selectors aren't available\n- Run Rstest CLI commands and inspect `.rstest/` artifacts to analyze failures\n- Clean up test data using `afterEach` to call cleanup API endpoints\n\n**NEVER:**\n- Test UI component rendering (use unit/integration tests)\n- Use brittle CSS selectors like `.class-name` or `#id`\n- Use `force:true` on `.click()` or `waitForTimeout`\n- Hard-code wait times (use `waitFor` with conditions)\n- Leave containers running after test failures\n- Commit test screenshots to git\n\n## Commands\n\n```bash\nbun run build                # Build frontend (REQUIRED first!)\nbun run e2e-test:variant console  # OSS E2E\nbun run e2e-test:variant console-enterprise  # Requires ENTERPRISE_BACKEND_DIR\nbun run e2e-test:variant console --watch  # Rstest watcher\nbun run e2e-test:variant console topics/topic-creation.spec.ts\nbun run e2e-test  # All four variants, including enterprise\n```\n\n## Test Architecture\n\n**OSS Mode (`bun run e2e-test:variant console`):** Redpanda + Backend + OwlShop containers\n**Enterprise Mode (`bun run e2e-test:variant console-enterprise`):** Same + RBAC, SSO (requires `console-enterprise` repo)\n\nFile location: `tests/test-variant-<variant>/<feature>/*.spec.ts`.\nImport `test`/`expect` from `tests/shared/e2e.ts` or the variant fixtures.\nSee [runner contracts](../../../tests/README.md).\n\n## Selector Priority\n\n1. `getByRole()` - Best for accessibility\n2. `getByLabel()` - For form inputs\n3. `getByText()` - For content verification\n4. `getByTestId()` - When semantic selectors aren't clear\n5. CSS selectors - Avoid if possible\n\n## Test ID Naming\n\n- kebab-case: `data-testid=\"feature-action-element\"`\n- Specific: include feature name + action + element type\n- Dynamic: `data-testid={\\`item-delete-\\${id}\\`}`\n\n## References\n\n- [Container Setup](references/container-setup.md) — Testcontainer lifecycle, configs, CI setup\n- [Test Patterns](references/test-patterns.md) — Multi-step workflows, forms, tables, API testing\n- [Failure Analysis](references/failure-analysis.md) — Error patterns and CLI debugging\n\n## Output\n\nAfter completing work:\n1. Confirm frontend build succeeded\n2. Verify all E2E tests pass\n3. Note any new test IDs added to components\n4. Mention cleanup of test containers\n","frontend/.claude/skills/e2e-tester/references/container-setup.md":"# Container setup reference\n\nThe maintained lifecycle lives in `tests/shared/global-setup.mjs`, shared by all four\nRstest variant configs. Read [Testcontainers details](../../../../tests/TESTCONTAINERS.md)\nand [test commands](../../../../tests/README.md) before changing it.\n\nBuild the frontend first. Setup copies its assets into the backend build context,\nbuilds an image, starts the configured brokers/services and backend, and waits for\nreadiness. The returned teardown owns live handles; `global-teardown.mjs` provides\nmanual crash recovery using the serialized state. Preserve failure cleanup.\n\nVariant metadata/configuration lives in `tests/test-variant-*/config/`. Images are\npinned in `test-images.json` at the repository root. Local single-variant runs remap host ports;\ncontainer-to-container addresses stay unchanged.\n\nEnterprise requires a separate backend checkout (`ENTERPRISE_BACKEND_DIR`). Global\nsetup authenticates through the real login form and stores browser state under the\ngitignored `playwright/.auth/` directory. Login-error specs override storage with an\nempty state. There is no HTTP Basic-auth replacement for application login.\n\nCI runs three OSS variants in `.github/workflows/frontend-verify.yml`. It installs\nChromium and Linux dependencies, downloads the same-run frontend build, and invokes\n`bun run e2e-test:variant <variant>`. Failure evidence is uploaded from `tests/.rstest/`.\nThe enterprise backend remains the responsibility of downstream enterprise CI.\n\nFor port conflicts, inspect the owner or use the dynamic-port wrapper. Never kill an\nunidentified developer process. For service failures, inspect Docker logs and the\nvariant state file rather than weakening waits or assertions.\n","frontend/.claude/skills/e2e-tester/references/failure-analysis.md":"# Failure Analysis Reference\n\n## CLI and artifacts\n\nRun `bun run e2e-test:variant console list --summary` to collect tests without services.\nRun `bun run e2e-test:variant console topics/topic-list.spec.ts` to reproduce against the\nreal stack. Inspect `.rstest/` screenshots, first-retry videos and Playwright traces.\nEnterprise screenshots/videos remain disabled. Use native locators/DOM inspection for\nselector diagnosis; do not replace real E2E coverage with mocked component tests.\n\n## Common Failure Patterns\n\n### 1. Element Not Found\n\n```\nError: locator.click: Target closed\nError: Timeout 30000ms exceeded waiting for locator\n```\n\n**Analysis steps:**\n1. Check if element has correct testid/role\n2. Verify element is visible (not hidden/collapsed)\n3. Check for timing issues (element loads async)\n4. Look for dynamic content that changes selector\n\n**Fix:**\n```typescript\n// BAD: Element might not be loaded\nawait page.getByRole('button', { name: 'Create' }).click();\n\n// GOOD: Wait for element to be visible\nawait expect(page.getByRole('button', { name: 'Create' })).toBeVisible();\nawait page.getByRole('button', { name: 'Create' }).click();\n\n// BETTER: Add testid for stability\nawait page.getByTestId('create-button').click();\n```\n\n### 2. Selector Ambiguity\n\n```\nError: strict mode violation: locator('button') resolved to 3 elements\n```\n\n**Fix:**\n```typescript\n// BAD: Multiple \"Edit\" buttons on page\nawait page.getByRole('button', { name: 'Edit' }).click();\n\n// GOOD: More specific with testid\nawait page.getByTestId('schema-edit-compatibility-btn').click();\n\n// GOOD: Scope within container\nawait page.getByRole('region', { name: 'Schema Details' })\n          .getByRole('button', { name: 'Edit' }).click();\n```\n\n### 3. Timing/Race Conditions\n\n```\nError: expect(locator).toHaveText()\nExpected string: \"Success\"\nReceived string: \"Loading...\"\n```\n\n**Fix:**\n```typescript\n// BAD: Doesn't wait for state change\nawait page.getByRole('button', { name: 'Save' }).click();\nexpect(page.getByText('Success')).toBeVisible();\n\n// GOOD: Wait for the expected state\nawait page.getByRole('button', { name: 'Save' }).click();\nawait expect(page.getByText('Success')).toBeVisible({ timeout: 5000 });\n```\n\n### 4. Navigation Issues\n\n```\nError: page.goto: net::ERR_CONNECTION_REFUSED\n```\n\n**Fix:**\n```bash\n# Check containers are running\ndocker ps | grep console-backend\n\n# Check container logs\ndocker logs <container-id>\n\n# Verify port mapping\ncurl http://localhost:3000\n\n# Check testcontainer state file\ncat .testcontainers-state.json\n```\n\n## Systematic Failure Analysis Workflow\n\n1. **Get Test Results** — Run the relevant variant CLI and inspect console/artifact output\n2. **Analyze Error Patterns:**\n   - Selector not found -> Missing/wrong testid or element not visible\n   - Strict mode violation -> Need more specific selector\n   - Timeout -> Element loads async, need waitFor\n   - Connection refused -> Container/service not running\n3. **Find Missing Test IDs** — Inspect semantic roles and labels in the source and browser\n4. **Add Test IDs** — Read component, add `data-testid`, follow naming convention\n5. **Update Tests** — Replace brittle selectors with stable testids, add wait conditions\n6. **Verify Fixes** — Run specific test file, then full suite\n\n## Debugging Commands\n\n```bash\n# Check container logs\ndocker ps -a | grep console-backend\ndocker logs <container-id>\n\n# Check if services are accessible\ncurl http://localhost:3000\ncurl http://localhost:19092\n\n# Run with debug output\nDEBUG=pw:api bun run e2e-test\n\n# Keep containers running on failure\nTESTCONTAINERS_RYUK_DISABLED=true bun run e2e-test\n```\n\n**In-test debugging:**\n```typescript\nawait page.pause();  // Opens Playwright Inspector\nawait page.screenshot({ path: 'debug.png' });\nconsole.log(await page.content());\n```\n\n## Test Timeout Issues\n\n```typescript\n// Increase timeout for slow operations\ntest('slow operation', async ({ page }) => {\n  test.setTimeout(60000); // 60 seconds\n  await page.goto('/slow-page');\n  await expect(page.getByText('Loaded')).toBeVisible({ timeout: 30000 });\n});\n```\n","frontend/.claude/skills/e2e-tester/references/test-patterns.md":"# Test Patterns Reference\n\n## Basic Test Structure\n\n```typescript\nimport { test, expect } from '../shared/e2e';\n\ntest.describe('Feature Name', () => {\n  test('user can complete workflow', async ({ page }) => {\n    await page.goto('/feature');\n    await page.getByRole('button', { name: 'Create' }).click();\n    await page.getByLabel('Name').fill('test-item');\n    await page.getByRole('button', { name: 'Submit' }).click();\n    await expect(page.getByText('Success')).toBeVisible();\n    await expect(page).toHaveURL(/\\/feature\\/test-item/);\n  });\n});\n```\n\n## Multi-Step Workflows\n\n```typescript\ntest('user creates, configures, and tests topic', async ({ page }) => {\n  // Step 1: Navigate and create\n  await page.goto('/topics');\n  await page.getByRole('button', { name: 'Create Topic' }).click();\n\n  // Step 2: Fill form\n  await page.getByLabel('Topic Name').fill('test-topic');\n  await page.getByLabel('Partitions').fill('3');\n  await page.getByRole('button', { name: 'Create' }).click();\n\n  // Step 3: Verify creation\n  await expect(page.getByText('Topic created successfully')).toBeVisible();\n  await expect(page).toHaveURL(/\\/topics\\/test-topic/);\n\n  // Step 4: Configure topic\n  await page.getByRole('button', { name: 'Configure' }).click();\n  await page.getByLabel('Retention Hours').fill('24');\n  await page.getByRole('button', { name: 'Save' }).click();\n\n  // Step 5: Verify configuration\n  await expect(page.getByText('Configuration saved')).toBeVisible();\n});\n```\n\n## Testing Forms\n\n```typescript\ntest('form validation works correctly', async ({ page }) => {\n  await page.goto('/create-topic');\n\n  // Submit empty form - should show errors\n  await page.getByRole('button', { name: 'Create' }).click();\n  await expect(page.getByText('Name is required')).toBeVisible();\n\n  // Fill valid data - should succeed\n  await page.getByLabel('Topic Name').fill('valid-topic');\n  await page.getByRole('button', { name: 'Create' }).click();\n  await expect(page.getByText('Success')).toBeVisible();\n});\n```\n\n## Testing Data Tables\n\n```typescript\ntest('user can filter and sort topics', async ({ page }) => {\n  await page.goto('/topics');\n\n  // Filter\n  await page.getByPlaceholder('Search topics').fill('test-');\n  await expect(page.getByRole('row')).toHaveCount(3); // Header + 2 results\n\n  // Sort\n  await page.getByRole('columnheader', { name: 'Name' }).click();\n  const firstRow = page.getByRole('row').nth(1);\n  await expect(firstRow).toContainText('test-topic-a');\n});\n```\n\n## API Interactions\n\n```typescript\ntest('creating topic triggers backend API', async ({ page }) => {\n  // Listen for API call\n  const apiPromise = page.waitForResponse(\n    resp => resp.url().includes('/api/topics') && resp.status() === 201\n  );\n\n  // Trigger action\n  await page.goto('/topics');\n  await page.getByRole('button', { name: 'Create Topic' }).click();\n  await page.getByLabel('Name').fill('api-test-topic');\n  await page.getByRole('button', { name: 'Create' }).click();\n\n  // Verify API was called\n  const response = await apiPromise;\n  const body = await response.json();\n  expect(body.name).toBe('api-test-topic');\n});\n```\n\n## Async Operations\n\n```typescript\n// Wait for specific condition\nawait expect(page.getByRole('status')).toHaveText('Ready');\n\n// Wait for navigation\nawait page.waitForURL('**/topics/my-topic');\n\n// Wait for API response\nawait page.waitForResponse(resp =>\n  resp.url().includes('/api/topics') && resp.status() === 200\n);\n\n// NEVER use fixed timeouts\n// await page.waitForTimeout(5000); // BAD\n```\n\n## Selectors Best Practices\n\n```typescript\n// GOOD: Role-based (accessible)\npage.getByRole('button', { name: 'Create Topic' })\npage.getByLabel('Topic Name')\npage.getByText('Success message')\n\n// GOOD: Test IDs when role isn't clear\npage.getByTestId('topic-list-item')\n\n// BAD: CSS selectors (brittle)\npage.locator('.btn-primary')\npage.locator('#topic-name-input')\n```\n\n## Adding Test IDs to Components\n\n```tsx\n// In React component\n<Button data-testid=\"create-topic-button\">Create Topic</Button>\n\n// In test\nawait page.getByTestId('create-topic-button').click();\n```\n\n**Where to Add:**\n1. Primary actions: Create, Save, Delete, Edit, Submit, Cancel buttons\n2. Navigation: Links to detail pages, breadcrumbs\n3. Forms: All input fields, selects, checkboxes, radio buttons\n4. Lists/Tables: Row identifiers, action buttons within rows\n5. Dialogs/Modals: Open/close buttons, form elements inside\n6. Search/Filter: Search inputs, filter dropdowns, clear buttons\n","frontend/.claude/skills/form-refactorer/SKILL.md":"---\nname: form-refactorer\ndescription: Refactor legacy forms to use modern Redpanda UI Registry Field components with react-hook-form and Zod validation. Use when user requests: (1) Form refactoring or modernization, (2) Updating forms to use Field components, (3) Migrating from legacy form patterns, (4) Implementing forms with react-hook-form and Zod validation.\n---\n\n# Form Refactorer\n\nRefactor legacy forms to modern Redpanda UI Registry Field components with react-hook-form and Zod validation.\n\n## Core Requirements\n\n### MUST USE (Modern Pattern)\n\n- Field component family from `components/redpanda-ui/components/field`\n- react-hook-form for form state management\n- Zod for validation schemas via `zodResolver`\n\n### FORBIDDEN (Legacy Patterns)\n\nSee [no-legacy](../code-standards/rules/no-legacy.md) for prohibited patterns. Also never import `Form` from `components/redpanda-ui/components/form`.\n\n## Component Hierarchy\n\n```\nFieldSet              - Semantic fieldset container for related fields\n├─ FieldLegend        - Title for the fieldset\n├─ FieldDescription   - Description for entire fieldset\n└─ FieldGroup         - Layout wrapper that stacks fields\n   ├─ Field           - Individual field wrapper\n   │  ├─ FieldLabel       - Label for input (use htmlFor)\n   │  ├─ Input/Select/... - Form control\n   │  ├─ FieldDescription - Helper text\n   │  └─ FieldError       - Error message\n   ├─ FieldSeparator  - Visual divider between fields\n   └─ Field           - Another field\n```\n\n## Key Patterns\n\n**Vertical field:**\n```tsx\n<Field data-invalid={!!errors.fieldName}>\n  <FieldLabel htmlFor=\"fieldName\">Label</FieldLabel>\n  <Input id=\"fieldName\" {...register(\"fieldName\")} aria-invalid={!!errors.fieldName} />\n  {errors.fieldName && <FieldError>{errors.fieldName.message}</FieldError>}\n</Field>\n```\n\n**Horizontal field (switch/checkbox):**\n```tsx\n<Field orientation=\"horizontal\">\n  <Switch id=\"toggle\" {...register(\"toggle\")} />\n  <FieldContent>\n    <FieldLabel htmlFor=\"toggle\">Enable Feature</FieldLabel>\n    <FieldDescription>Description</FieldDescription>\n  </FieldContent>\n</Field>\n```\n\n## Checklist\n\nBefore completing refactoring, verify:\n\n- [ ] All legacy form imports removed (the legacy `Form` component)\n- [ ] Field components used for all fields\n- [ ] FieldLabel has `htmlFor` matching Input `id`\n- [ ] Errors show `data-invalid` on Field and `aria-invalid` on Input\n- [ ] Yup replaced with Zod + zodResolver\n- [ ] FieldSet + FieldLegend used for semantic grouping\n- [ ] FieldError conditionally rendered: `{errors.field && <FieldError>...`\n- [ ] Form submission handled via react-hook-form `handleSubmit`\n\n## References\n\n- [Common Patterns](references/common-patterns.md) — Dynamic fields, field arrays, multi-section forms, pitfalls, Zod patterns\n- [Field Component Docs](https://redpanda-ui-registry.netlify.app/docs/field)\n","frontend/.claude/skills/form-refactorer/references/common-patterns.md":"# Common Form Patterns\n\nThis document provides patterns for common form scenarios using Field components.\n\n## Dynamic Fields\n\nForms with conditionally rendered fields based on user input.\n\n```tsx\nimport { useForm, useWatch } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { z } from \"zod\";\nimport {\n  FieldSet,\n  FieldLegend,\n  FieldGroup,\n  Field,\n  FieldLabel,\n  FieldError\n} from \"components/redpanda-ui/components/field\";\nimport { Select } from \"components/redpanda-ui/components/select\";\nimport { Input } from \"components/redpanda-ui/components/input\";\n\nconst schema = z.object({\n  authType: z.enum([\"none\", \"basic\", \"token\"]),\n  username: z.string().optional(),\n  password: z.string().optional(),\n  token: z.string().optional(),\n}).refine((data) => {\n  if (data.authType === \"basic\") {\n    return !!data.username && !!data.password;\n  }\n  if (data.authType === \"token\") {\n    return !!data.token;\n  }\n  return true;\n}, {\n  message: \"Authentication fields are required\",\n});\n\nexport const AuthForm = () => {\n  const { register, control, formState: { errors } } = useForm({\n    resolver: zodResolver(schema),\n  });\n\n  const authType = useWatch({ control, name: \"authType\" });\n\n  return (\n    <FieldSet>\n      <FieldLegend>Authentication</FieldLegend>\n      <FieldGroup>\n        <Field data-invalid={!!errors.authType}>\n          <FieldLabel htmlFor=\"authType\">Authentication Type</FieldLabel>\n          <Select id=\"authType\" {...register(\"authType\")}>\n            <option value=\"none\">None</option>\n            <option value=\"basic\">Basic Auth</option>\n            <option value=\"token\">Token</option>\n          </Select>\n          {errors.authType && <FieldError>{errors.authType.message}</FieldError>}\n        </Field>\n\n        {authType === \"basic\" && (\n          <>\n            <Field data-invalid={!!errors.username}>\n              <FieldLabel htmlFor=\"username\">Username</FieldLabel>\n              <Input id=\"username\" {...register(\"username\")} aria-invalid={!!errors.username} />\n              {errors.username && <FieldError>{errors.username.message}</FieldError>}\n            </Field>\n\n            <Field data-invalid={!!errors.password}>\n              <FieldLabel htmlFor=\"password\">Password</FieldLabel>\n              <Input id=\"password\" type=\"password\" {...register(\"password\")} aria-invalid={!!errors.password} />\n              {errors.password && <FieldError>{errors.password.message}</FieldError>}\n            </Field>\n          </>\n        )}\n\n        {authType === \"token\" && (\n          <Field data-invalid={!!errors.token}>\n            <FieldLabel htmlFor=\"token\">API Token</FieldLabel>\n            <Input id=\"token\" {...register(\"token\")} aria-invalid={!!errors.token} />\n            {errors.token && <FieldError>{errors.token.message}</FieldError>}\n          </Field>\n        )}\n      </FieldGroup>\n    </FieldSet>\n  );\n};\n```\n\n## Field Arrays (Repeatable Fields)\n\nForms with dynamically added/removed field groups.\n\n```tsx\nimport { useForm, useFieldArray } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { z } from \"zod\";\nimport {\n  FieldSet,\n  FieldLegend,\n  FieldGroup,\n  Field,\n  FieldLabel,\n  FieldError,\n  FieldSeparator\n} from \"components/redpanda-ui/components/field\";\nimport { Input } from \"components/redpanda-ui/components/input\";\nimport { Button } from \"components/redpanda-ui/components/button\";\n\nconst schema = z.object({\n  tags: z.array(z.object({\n    key: z.string().min(1, \"Key is required\"),\n    value: z.string().min(1, \"Value is required\"),\n  })).min(1, \"At least one tag is required\"),\n});\n\nexport const TagsForm = () => {\n  const { register, control, formState: { errors } } = useForm({\n    resolver: zodResolver(schema),\n    defaultValues: {\n      tags: [{ key: \"\", value: \"\" }],\n    },\n  });\n\n  const { fields, append, remove } = useFieldArray({\n    control,\n    name: \"tags\",\n  });\n\n  return (\n    <FieldSet>\n      <FieldLegend>Tags</FieldLegend>\n      <FieldGroup>\n        {fields.map((field, index) => (\n          <div key={field.id}>\n            {index > 0 && <FieldSeparator />}\n            <FieldGroup>\n              <Field data-invalid={!!errors.tags?.[index]?.key}>\n                <FieldLabel htmlFor={`tags.${index}.key`}>Key</FieldLabel>\n                <Input\n                  id={`tags.${index}.key`}\n                  {...register(`tags.${index}.key`)}\n                  aria-invalid={!!errors.tags?.[index]?.key}\n                />\n                {errors.tags?.[index]?.key && (\n                  <FieldError>{errors.tags[index].key.message}</FieldError>\n                )}\n              </Field>\n\n              <Field data-invalid={!!errors.tags?.[index]?.value}>\n                <FieldLabel htmlFor={`tags.${index}.value`}>Value</FieldLabel>\n                <Input\n                  id={`tags.${index}.value`}\n                  {...register(`tags.${index}.value`)}\n                  aria-invalid={!!errors.tags?.[index]?.value}\n                />\n                {errors.tags?.[index]?.value && (\n                  <FieldError>{errors.tags[index].value.message}</FieldError>\n                )}\n              </Field>\n\n              {fields.length > 1 && (\n                <Button\n                  type=\"button\"\n                  variant=\"destructive\"\n                  onClick={() => remove(index)}\n                >\n                  Remove Tag\n                </Button>\n              )}\n            </FieldGroup>\n          </div>\n        ))}\n\n        <Button\n          type=\"button\"\n          variant=\"secondary\"\n          onClick={() => append({ key: \"\", value: \"\" })}\n        >\n          Add Tag\n        </Button>\n      </FieldGroup>\n    </FieldSet>\n  );\n};\n```\n\n## Multi-Section Forms\n\nForms organized into multiple logical sections.\n\n```tsx\nimport {\n  FieldSet,\n  FieldLegend,\n  FieldDescription,\n  FieldGroup,\n  Field,\n  FieldLabel,\n  FieldSeparator,\n  FieldError\n} from \"components/redpanda-ui/components/field\";\nimport { Input } from \"components/redpanda-ui/components/input\";\nimport { Textarea } from \"components/redpanda-ui/components/textarea\";\nimport { Switch } from \"components/redpanda-ui/components/switch\";\n\nexport const ProfileSettingsForm = () => {\n  const { register, formState: { errors } } = useForm({\n    resolver: zodResolver(schema),\n  });\n\n  return (\n    <form>\n      {/* Personal Information Section */}\n      <FieldSet>\n        <FieldLegend>Personal Information</FieldLegend>\n        <FieldDescription>\n          This information will be displayed on your public profile.\n        </FieldDescription>\n        <FieldGroup>\n          <Field data-invalid={!!errors.name}>\n            <FieldLabel htmlFor=\"name\">Full Name</FieldLabel>\n            <Input id=\"name\" {...register(\"name\")} aria-invalid={!!errors.name} />\n            {errors.name && <FieldError>{errors.name.message}</FieldError>}\n          </Field>\n\n          <Field data-invalid={!!errors.bio}>\n            <FieldLabel htmlFor=\"bio\">Bio</FieldLabel>\n            <Textarea id=\"bio\" {...register(\"bio\")} aria-invalid={!!errors.bio} />\n            {errors.bio && <FieldError>{errors.bio.message}</FieldError>}\n          </Field>\n        </FieldGroup>\n      </FieldSet>\n\n      <FieldSeparator />\n\n      {/* Notification Preferences Section */}\n      <FieldSet>\n        <FieldLegend>Notification Preferences</FieldLegend>\n        <FieldDescription>\n          Manage how you receive notifications.\n        </FieldDescription>\n        <FieldGroup>\n          <Field orientation=\"horizontal\">\n            <Switch id=\"emailNotifications\" {...register(\"emailNotifications\")} />\n            <FieldContent>\n              <FieldLabel htmlFor=\"emailNotifications\">Email Notifications</FieldLabel>\n              <FieldDescription>Receive notifications via email</FieldDescription>\n            </FieldContent>\n          </Field>\n\n          <Field orientation=\"horizontal\">\n            <Switch id=\"pushNotifications\" {...register(\"pushNotifications\")} />\n            <FieldContent>\n              <FieldLabel htmlFor=\"pushNotifications\">Push Notifications</FieldLabel>\n              <FieldDescription>Receive push notifications in browser</FieldDescription>\n            </FieldContent>\n          </Field>\n        </FieldGroup>\n      </FieldSet>\n    </form>\n  );\n};\n```\n\n## Read-Only Fields\n\nDisplay form fields in a read-only state.\n\n```tsx\nimport {\n  Field,\n  FieldLabel,\n  FieldDescription\n} from \"components/redpanda-ui/components/field\";\nimport { Input } from \"components/redpanda-ui/components/input\";\n\nexport const ServerDetailsForm = ({ server }) => {\n  return (\n    <FieldGroup>\n      <Field>\n        <FieldLabel htmlFor=\"id\">Server ID</FieldLabel>\n        <Input id=\"id\" value={server.id} disabled />\n        <FieldDescription>Auto-generated unique identifier</FieldDescription>\n      </Field>\n\n      <Field>\n        <FieldLabel htmlFor=\"createdAt\">Created At</FieldLabel>\n        <Input id=\"createdAt\" value={server.createdAt} disabled />\n      </Field>\n    </FieldGroup>\n  );\n};\n```\n\n## Inline Form Actions\n\nForms with actions positioned within the field group.\n\n```tsx\nimport {\n  FieldSet,\n  FieldLegend,\n  FieldGroup,\n  Field,\n  FieldLabel,\n  FieldError\n} from \"components/redpanda-ui/components/field\";\nimport { Input } from \"components/redpanda-ui/components/input\";\nimport { Button } from \"components/redpanda-ui/components/button\";\n\nexport const InlineEditForm = () => {\n  const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm();\n\n  return (\n    <form onSubmit={handleSubmit(onSubmit)}>\n      <FieldSet>\n        <FieldLegend>Quick Edit</FieldLegend>\n        <FieldGroup>\n          <Field data-invalid={!!errors.name}>\n            <FieldLabel htmlFor=\"name\">Name</FieldLabel>\n            <div className=\"flex gap-2\">\n              <Input\n                id=\"name\"\n                {...register(\"name\")}\n                aria-invalid={!!errors.name}\n                className=\"flex-1\"\n              />\n              <Button type=\"submit\" disabled={isSubmitting}>\n                Save\n              </Button>\n            </div>\n            {errors.name && <FieldError>{errors.name.message}</FieldError>}\n          </Field>\n        </FieldGroup>\n      </FieldSet>\n    </form>\n  );\n};\n```\n\n## Common Zod Patterns\n\n```tsx\n// String validations\nz.string().min(1, \"Required\")\nz.string().email(\"Invalid email\")\nz.string().url(\"Invalid URL\")\nz.string().regex(/^[A-Z]/, \"Must start with capital\")\n\n// Number validations\nz.number().min(0, \"Must be positive\")\nz.number().max(100, \"Max 100\")\nz.number().int(\"Must be integer\")\n\n// Optional fields\nz.string().optional()\nz.string().nullable()\n\n// Arrays\nz.array(z.string()).min(1, \"At least one required\")\n\n// Objects\nz.object({\n  nested: z.string(),\n})\n\n// Conditional validation\nz.object({\n  type: z.enum([\"a\", \"b\"]),\n  value: z.string(),\n}).refine((data) => {\n  if (data.type === \"a\") return data.value.length > 5;\n  return true;\n}, \"Value must be longer than 5 for type A\")\n```\n\n## Common Pitfalls\n\n### Pitfall 1: Missing htmlFor/id Association\n\n```tsx\n// WRONG:\n<Field>\n  <FieldLabel>Email</FieldLabel>\n  <Input name=\"email\" />\n</Field>\n\n// CORRECT:\n<Field>\n  <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n  <Input id=\"email\" name=\"email\" />\n</Field>\n```\n\n### Pitfall 2: Missing aria-invalid\n\n```tsx\n// WRONG:\n<Field data-invalid={!!errors.email}>\n  <Input {...register(\"email\")} />\n</Field>\n\n// CORRECT:\n<Field data-invalid={!!errors.email}>\n  <Input {...register(\"email\")} aria-invalid={!!errors.email} />\n</Field>\n```\n\n### Pitfall 3: Conditional FieldError Without Check\n\n```tsx\n// WRONG:\n<FieldError>{errors.email.message}</FieldError>\n\n// CORRECT:\n{errors.email && <FieldError>{errors.email.message}</FieldError>}\n```\n\n### Pitfall 4: Using Legacy Form Components\n\n```tsx\n// WRONG:\nimport { Form } from \"components/redpanda-ui/components/form\";\n\n// CORRECT:\nimport { Field, FieldLabel } from \"components/redpanda-ui/components/field\";\n```\n\n### Pitfall 5: Forgetting FieldContent for Horizontal Fields\n\n```tsx\n// WRONG:\n<Field orientation=\"horizontal\">\n  <Switch id=\"notifications\" />\n  <FieldLabel htmlFor=\"notifications\">Enable</FieldLabel>\n  <FieldDescription>Receive updates</FieldDescription>\n</Field>\n\n// CORRECT:\n<Field orientation=\"horizontal\">\n  <Switch id=\"notifications\" />\n  <FieldContent>\n    <FieldLabel htmlFor=\"notifications\">Enable</FieldLabel>\n    <FieldDescription>Receive updates</FieldDescription>\n  </FieldContent>\n</Field>\n```\n","frontend/.claude/skills/react-best-practices/SKILL.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Freact-best-practices%2FSKILL.md -->","frontend/.claude/skills/react-best-practices/rules/advanced-event-handler-refs.md":"---\ntitle: Store Event Handlers in Refs\nimpact: LOW\nimpactDescription: stable subscriptions\ntags: advanced, hooks, refs, event-handlers, optimization\n---\n\n## Store Event Handlers in Refs\n\nStore callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.\n\n**Incorrect (re-subscribes on every render):**\n\n```tsx\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  useEffect(() => {\n    window.addEventListener(event, handler)\n    return () => window.removeEventListener(event, handler)\n  }, [event, handler])\n}\n```\n\n**Correct (stable subscription):**\n\n```tsx\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  const handlerRef = useRef(handler)\n  useEffect(() => {\n    handlerRef.current = handler\n  }, [handler])\n\n  useEffect(() => {\n    const listener = (e) => handlerRef.current(e)\n    window.addEventListener(event, listener)\n    return () => window.removeEventListener(event, listener)\n  }, [event])\n}\n```\n\n**Alternative: use `useEffectEvent` if you're on latest React:**\n\n```tsx\nimport { useEffectEvent } from 'react'\n\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  const onEvent = useEffectEvent(handler)\n\n  useEffect(() => {\n    window.addEventListener(event, onEvent)\n    return () => window.removeEventListener(event, onEvent)\n  }, [event])\n}\n```\n\n`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.\n","frontend/.claude/skills/react-best-practices/rules/advanced-use-latest.md":"---\ntitle: useLatest for Stable Callback Refs\nimpact: LOW\nimpactDescription: prevents effect re-runs\ntags: advanced, hooks, useLatest, refs, optimization\n---\n\n## useLatest for Stable Callback Refs\n\nAccess latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.\n\n**Implementation:**\n\n```typescript\nfunction useLatest<T>(value: T) {\n  const ref = useRef(value)\n  useLayoutEffect(() => {\n    ref.current = value\n  }, [value])\n  return ref\n}\n```\n\n**Incorrect (effect re-runs on every callback change):**\n\n```tsx\nfunction SearchInput({ onSearch }: { onSearch: (q: string) => void }) {\n  const [query, setQuery] = useState('')\n\n  useEffect(() => {\n    const timeout = setTimeout(() => onSearch(query), 300)\n    return () => clearTimeout(timeout)\n  }, [query, onSearch])\n}\n```\n\n**Correct (stable effect, fresh callback):**\n\n```tsx\nfunction SearchInput({ onSearch }: { onSearch: (q: string) => void }) {\n  const [query, setQuery] = useState('')\n  const onSearchRef = useLatest(onSearch)\n\n  useEffect(() => {\n    const timeout = setTimeout(() => onSearchRef.current(query), 300)\n    return () => clearTimeout(timeout)\n  }, [query])\n}\n```\n","frontend/.claude/skills/react-best-practices/rules/async-defer-await.md":"---\ntitle: Defer Await Until Needed\nimpact: HIGH\nimpactDescription: avoids blocking unused code paths\ntags: async, await, conditional, optimization\n---\n\n## Defer Await Until Needed\n\nMove `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.\n\n**Incorrect (blocks both branches):**\n\n```typescript\nasync function handleRequest(userId: string, skipProcessing: boolean) {\n  const userData = await fetchUserData(userId)\n  \n  if (skipProcessing) {\n    // Returns immediately but still waited for userData\n    return { skipped: true }\n  }\n  \n  // Only this branch uses userData\n  return processUserData(userData)\n}\n```\n\n**Correct (only blocks when needed):**\n\n```typescript\nasync function handleRequest(userId: string, skipProcessing: boolean) {\n  if (skipProcessing) {\n    // Returns immediately without waiting\n    return { skipped: true }\n  }\n  \n  // Fetch only when needed\n  const userData = await fetchUserData(userId)\n  return processUserData(userData)\n}\n```\n\n**Another example (early return optimization):**\n\n```typescript\n// Incorrect: always fetches permissions\nasync function updateResource(resourceId: string, userId: string) {\n  const permissions = await fetchPermissions(userId)\n  const resource = await getResource(resourceId)\n  \n  if (!resource) {\n    return { error: 'Not found' }\n  }\n  \n  if (!permissions.canEdit) {\n    return { error: 'Forbidden' }\n  }\n  \n  return await updateResourceData(resource, permissions)\n}\n\n// Correct: fetches only when needed\nasync function updateResource(resourceId: string, userId: string) {\n  const resource = await getResource(resourceId)\n  \n  if (!resource) {\n    return { error: 'Not found' }\n  }\n  \n  const permissions = await fetchPermissions(userId)\n  \n  if (!permissions.canEdit) {\n    return { error: 'Forbidden' }\n  }\n  \n  return await updateResourceData(resource, permissions)\n}\n```\n\nThis optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.\n","frontend/.claude/skills/react-best-practices/rules/async-dependencies.md":"---\ntitle: Dependency-Based Parallelization\nimpact: CRITICAL\nimpactDescription: 2-10× improvement\ntags: async, parallelization, dependencies, better-all\n---\n\n## Dependency-Based Parallelization\n\nFor operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.\n\n**Incorrect (profile waits for config unnecessarily):**\n\n```typescript\nconst [user, config] = await Promise.all([\n  fetchUser(),\n  fetchConfig()\n])\nconst profile = await fetchProfile(user.id)\n```\n\n**Correct (config and profile run in parallel):**\n\n```typescript\nimport { all } from 'better-all'\n\nconst { user, config, profile } = await all({\n  async user() { return fetchUser() },\n  async config() { return fetchConfig() },\n  async profile() {\n    return fetchProfile((await this.$.user).id)\n  }\n})\n```\n\nReference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)\n","frontend/.claude/skills/react-best-practices/rules/async-suspense-boundaries.md":"---\ntitle: Strategic Suspense Boundaries\nimpact: HIGH\nimpactDescription: faster initial paint\ntags: async, suspense, streaming, layout-shift\n---\n\n## Strategic Suspense Boundaries\n\nInstead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.\n\n**Incorrect (wrapper blocked by data fetching):**\n\n```tsx\nasync function Page() {\n  const data = await fetchData() // Blocks entire page\n  \n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <div>\n        <DataDisplay data={data} />\n      </div>\n      <div>Footer</div>\n    </div>\n  )\n}\n```\n\nThe entire layout waits for data even though only the middle section needs it.\n\n**Correct (wrapper shows immediately, data streams in):**\n\n```tsx\nfunction Page() {\n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <div>\n        <Suspense fallback={<Skeleton />}>\n          <DataDisplay />\n        </Suspense>\n      </div>\n      <div>Footer</div>\n    </div>\n  )\n}\n\nasync function DataDisplay() {\n  const data = await fetchData() // Only blocks this component\n  return <div>{data.content}</div>\n}\n```\n\nSidebar, Header, and Footer render immediately. Only DataDisplay waits for data.\n\n**Alternative (share promise across components):**\n\n```tsx\nfunction Page() {\n  // Start fetch immediately, but don't await\n  const dataPromise = fetchData()\n  \n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <Suspense fallback={<Skeleton />}>\n        <DataDisplay dataPromise={dataPromise} />\n        <DataSummary dataPromise={dataPromise} />\n      </Suspense>\n      <div>Footer</div>\n    </div>\n  )\n}\n\nfunction DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {\n  const data = use(dataPromise) // Unwraps the promise\n  return <div>{data.content}</div>\n}\n\nfunction DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {\n  const data = use(dataPromise) // Reuses the same promise\n  return <div>{data.summary}</div>\n}\n```\n\nBoth components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.\n\n**When NOT to use this pattern:**\n\n- Critical data needed for layout decisions (affects positioning)\n- SEO-critical content above the fold\n- Small, fast queries where suspense overhead isn't worth it\n- When you want to avoid layout shift (loading → content jump)\n\n**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.\n","frontend/.claude/skills/react-best-practices/rules/bundle-barrel-imports.md":"---\ntitle: Avoid Barrel File Imports\nimpact: CRITICAL\nimpactDescription: 200-800ms import cost, slow builds\ntags: bundle, imports, tree-shaking, barrel-files, performance\n---\n\n## Avoid Barrel File Imports\n\nImport directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).\n\nPopular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.\n\n**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.\n\n**Incorrect (imports entire library):**\n\n```tsx\nimport { Check, X, Menu } from 'lucide-react'\n// Loads 1,583 modules, takes ~2.8s extra in dev\n// Runtime cost: 200-800ms on every cold start\n\nimport { Button, TextField } from '@mui/material'\n// Loads 2,225 modules, takes ~4.2s extra in dev\n```\n\n**Correct (imports only what you need):**\n\n```tsx\nimport Check from 'lucide-react/dist/esm/icons/check'\nimport X from 'lucide-react/dist/esm/icons/x'\nimport Menu from 'lucide-react/dist/esm/icons/menu'\n// Loads only 3 modules (~2KB vs ~1MB)\n\nimport Button from '@mui/material/Button'\nimport TextField from '@mui/material/TextField'\n// Loads only what you use\n```\n\nDirect imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.\n\nLibraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.\n\nReference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)\n","frontend/.claude/skills/react-best-practices/rules/bundle-code-splitting.md":"---\ntitle: Code Splitting and Lazy Loading\nimpact: CRITICAL\nimpactDescription: directly affects TTI and LCP\ntags: bundle, dynamic-import, code-splitting, react-lazy, conditional-loading\n---\n\n## Code Splitting and Lazy Loading\n\nUse `React.lazy` with `Suspense` to lazy-load large components not needed on initial render. Load large data modules conditionally. Defer non-critical third-party libraries.\n\n### Dynamic Imports for Heavy Components\n\n**Incorrect (Monaco bundles with main chunk ~300KB):**\n\n```tsx\nimport { MonacoEditor } from './monaco-editor'\n```\n\n**Correct (Monaco loads on demand):**\n\n```tsx\nimport { lazy, Suspense } from 'react'\n\nconst MonacoEditor = lazy(() =>\n  import('./monaco-editor').then(m => ({ default: m.MonacoEditor }))\n)\n\nfunction CodePanel({ code }: { code: string }) {\n  return (\n    <Suspense fallback={<div>Loading editor...</div>}>\n      <MonacoEditor value={code} />\n    </Suspense>\n  )\n}\n```\n\n### Conditional Module Loading\n\nLoad large data or modules only when a feature is activated:\n\n```tsx\nfunction AnimationPlayer({ enabled, setEnabled }: Props) {\n  const [frames, setFrames] = useState<Frame[] | null>(null)\n\n  useEffect(() => {\n    if (enabled && !frames) {\n      import('./animation-frames.js')\n        .then(mod => setFrames(mod.frames))\n        .catch(() => setEnabled(false))\n    }\n  }, [enabled, frames, setEnabled])\n\n  if (!frames) return <Skeleton />\n  return <Canvas frames={frames} />\n}\n```\n\n### Defer Non-Critical Third-Party Libraries\n\nAnalytics, logging, and error tracking don't block user interaction:\n\n```tsx\nconst Analytics = lazy(() =>\n  import('@some-vendor/analytics').then(m => ({ default: m.Analytics }))\n)\n\nexport function App({ children }) {\n  return (\n    <div>\n      {children}\n      <Suspense fallback={null}>\n        <Analytics />\n      </Suspense>\n    </div>\n  )\n}\n```\n","frontend/.claude/skills/react-best-practices/rules/bundle-preload.md":"---\ntitle: Preload Based on User Intent\nimpact: MEDIUM\nimpactDescription: reduces perceived latency\ntags: bundle, preload, user-intent, hover\n---\n\n## Preload Based on User Intent\n\nPreload heavy bundles before they're needed to reduce perceived latency.\n\n**Example (preload on hover/focus):**\n\n```tsx\nfunction EditorButton({ onClick }: { onClick: () => void }) {\n  const preload = () => {\n    if (typeof window !== 'undefined') {\n      void import('./monaco-editor')\n    }\n  }\n\n  return (\n    <button\n      onMouseEnter={preload}\n      onFocus={preload}\n      onClick={onClick}\n    >\n      Open Editor\n    </button>\n  )\n}\n```\n\n**Example (preload when feature flag is enabled):**\n\n```tsx\nfunction FlagsProvider({ children, flags }: Props) {\n  useEffect(() => {\n    if (flags.editorEnabled && typeof window !== 'undefined') {\n      void import('./monaco-editor').then(mod => mod.init())\n    }\n  }, [flags.editorEnabled])\n\n  return <FlagsContext.Provider value={flags}>\n    {children}\n  </FlagsContext.Provider>\n}\n```\n\nThe `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.\n","frontend/.claude/skills/react-best-practices/rules/client-event-listeners.md":"---\ntitle: Deduplicate Global Event Listeners\nimpact: LOW\nimpactDescription: single listener for N components\ntags: client, swr, event-listeners, subscription\n---\n\n## Deduplicate Global Event Listeners\n\nUse `useSWRSubscription()` to share global event listeners across component instances.\n\n**Incorrect (N instances = N listeners):**\n\n```tsx\nfunction useKeyboardShortcut(key: string, callback: () => void) {\n  useEffect(() => {\n    const handler = (e: KeyboardEvent) => {\n      if (e.metaKey && e.key === key) {\n        callback()\n      }\n    }\n    window.addEventListener('keydown', handler)\n    return () => window.removeEventListener('keydown', handler)\n  }, [key, callback])\n}\n```\n\nWhen using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.\n\n**Correct (N instances = 1 listener):**\n\n```tsx\nimport useSWRSubscription from 'swr/subscription'\n\n// Module-level Map to track callbacks per key\nconst keyCallbacks = new Map<string, Set<() => void>>()\n\nfunction useKeyboardShortcut(key: string, callback: () => void) {\n  // Register this callback in the Map\n  useEffect(() => {\n    if (!keyCallbacks.has(key)) {\n      keyCallbacks.set(key, new Set())\n    }\n    keyCallbacks.get(key)!.add(callback)\n\n    return () => {\n      const set = keyCallbacks.get(key)\n      if (set) {\n        set.delete(callback)\n        if (set.size === 0) {\n          keyCallbacks.delete(key)\n        }\n      }\n    }\n  }, [key, callback])\n\n  useSWRSubscription('global-keydown', () => {\n    const handler = (e: KeyboardEvent) => {\n      if (e.metaKey && keyCallbacks.has(e.key)) {\n        keyCallbacks.get(e.key)!.forEach(cb => cb())\n      }\n    }\n    window.addEventListener('keydown', handler)\n    return () => window.removeEventListener('keydown', handler)\n  })\n}\n\nfunction Profile() {\n  // Multiple shortcuts will share the same listener\n  useKeyboardShortcut('p', () => { /* ... */ }) \n  useKeyboardShortcut('k', () => { /* ... */ })\n  // ...\n}\n```\n","frontend/.claude/skills/react-best-practices/rules/client-passive-event-listeners.md":"---\ntitle: Use Passive Event Listeners for Scrolling Performance\nimpact: MEDIUM\nimpactDescription: eliminates scroll delay caused by event listeners\ntags: client, event-listeners, scrolling, performance, touch, wheel\n---\n\n## Use Passive Event Listeners for Scrolling Performance\n\nAdd `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay.\n\n**Incorrect:**\n\n```typescript\nuseEffect(() => {\n  const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)\n  const handleWheel = (e: WheelEvent) => console.log(e.deltaY)\n  \n  document.addEventListener('touchstart', handleTouch)\n  document.addEventListener('wheel', handleWheel)\n  \n  return () => {\n    document.removeEventListener('touchstart', handleTouch)\n    document.removeEventListener('wheel', handleWheel)\n  }\n}, [])\n```\n\n**Correct:**\n\n```typescript\nuseEffect(() => {\n  const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)\n  const handleWheel = (e: WheelEvent) => console.log(e.deltaY)\n  \n  document.addEventListener('touchstart', handleTouch, { passive: true })\n  document.addEventListener('wheel', handleWheel, { passive: true })\n  \n  return () => {\n    document.removeEventListener('touchstart', handleTouch)\n    document.removeEventListener('wheel', handleWheel)\n  }\n}, [])\n```\n\n**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.\n\n**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.\n","frontend/.claude/skills/react-best-practices/rules/js-batch-dom-css.md":"---\ntitle: Batch DOM CSS Changes\nimpact: MEDIUM\nimpactDescription: reduces reflows/repaints\ntags: javascript, dom, css, performance, reflow\n---\n\n## Batch DOM CSS Changes\n\nAvoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.\n\n**Incorrect (interleaved reads and writes force reflows):**\n\n```typescript\nfunction updateElementStyles(element: HTMLElement) {\n  element.style.width = '100px'\n  const width = element.offsetWidth  // Forces reflow\n  element.style.height = '200px'\n  const height = element.offsetHeight  // Forces another reflow\n}\n```\n\n**Correct (batch writes, then read once):**\n\n```typescript\nfunction updateElementStyles(element: HTMLElement) {\n  // Batch all writes together\n  element.style.width = '100px'\n  element.style.height = '200px'\n  element.style.backgroundColor = 'blue'\n  element.style.border = '1px solid black'\n  \n  // Read after all writes are done (single reflow)\n  const { width, height } = element.getBoundingClientRect()\n}\n```\n\n**Better: use CSS classes**\n\n```css\n.highlighted-box {\n  width: 100px;\n  height: 200px;\n  background-color: blue;\n  border: 1px solid black;\n}\n```\n\n```typescript\nfunction updateElementStyles(element: HTMLElement) {\n  element.classList.add('highlighted-box')\n\n  const { width, height } = element.getBoundingClientRect()\n}\n```\n\nPrefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.","frontend/.claude/skills/react-best-practices/rules/js-caching-patterns.md":"---\ntitle: Client-Side Caching Patterns\nimpact: MEDIUM\nimpactDescription: reduces redundant computation and expensive I/O\ntags: javascript, cache, memoization, localStorage, storage, performance\n---\n\n## Client-Side Caching Patterns\n\n### Cache Repeated Function Calls\n\nUse a module-level Map to cache function results when the same function is called repeatedly with the same inputs.\n\n```typescript\nconst slugifyCache = new Map<string, string>()\n\nfunction cachedSlugify(text: string): string {\n  if (slugifyCache.has(text)) return slugifyCache.get(text)!\n  const result = slugify(text)\n  slugifyCache.set(text, result)\n  return result\n}\n```\n\nUse a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.\n\n### Cache Storage API Calls\n\n`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory:\n\n```typescript\nconst storageCache = new Map<string, string | null>()\n\nfunction getLocalStorage(key: string) {\n  if (!storageCache.has(key)) {\n    storageCache.set(key, localStorage.getItem(key))\n  }\n  return storageCache.get(key)\n}\n\nfunction setLocalStorage(key: string, value: string) {\n  localStorage.setItem(key, value)\n  storageCache.set(key, value)\n}\n```\n\nInvalidate on external changes:\n\n```typescript\nwindow.addEventListener('storage', (e) => {\n  if (e.key) storageCache.delete(e.key)\n})\n```\n\n### Version localStorage Data\n\nAdd version prefix to keys and store only needed fields:\n\n```typescript\nconst VERSION = 'v2'\n\nfunction saveConfig(config: { theme: string; language: string }) {\n  try {\n    localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))\n  } catch {\n    // Throws in incognito, quota exceeded, or disabled\n  }\n}\n```\n\nAlways wrap `getItem()`/`setItem()` in try-catch — they throw in incognito/private browsing (Safari, Firefox).\n\n## Reference\n\n- [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)\n","frontend/.claude/skills/react-best-practices/rules/js-index-maps.md":"---\ntitle: Build Index Maps for Repeated Lookups\nimpact: LOW-MEDIUM\nimpactDescription: 1M ops to 2K ops\ntags: javascript, map, indexing, optimization, performance\n---\n\n## Build Index Maps for Repeated Lookups\n\nMultiple `.find()` calls by the same key should use a Map.\n\n**Incorrect (O(n) per lookup):**\n\n```typescript\nfunction processOrders(orders: Order[], users: User[]) {\n  return orders.map(order => ({\n    ...order,\n    user: users.find(u => u.id === order.userId)\n  }))\n}\n```\n\n**Correct (O(1) per lookup):**\n\n```typescript\nfunction processOrders(orders: Order[], users: User[]) {\n  const userById = new Map(users.map(u => [u.id, u]))\n\n  return orders.map(order => ({\n    ...order,\n    user: userById.get(order.userId)\n  }))\n}\n```\n\nBuild map once (O(n)), then all lookups are O(1).\nFor 1000 orders × 1000 users: 1M ops → 2K ops.\n","frontend/.claude/skills/react-best-practices/rules/js-length-check-first.md":"---\ntitle: Early Length Check for Array Comparisons\nimpact: MEDIUM-HIGH\nimpactDescription: avoids expensive operations when lengths differ\ntags: javascript, arrays, performance, optimization, comparison\n---\n\n## Early Length Check for Array Comparisons\n\nWhen comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.\n\nIn real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).\n\n**Incorrect (always runs expensive comparison):**\n\n```typescript\nfunction hasChanges(current: string[], original: string[]) {\n  // Always sorts and joins, even when lengths differ\n  return current.sort().join() !== original.sort().join()\n}\n```\n\nTwo O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.\n\n**Correct (O(1) length check first):**\n\n```typescript\nfunction hasChanges(current: string[], original: string[]) {\n  // Early return if lengths differ\n  if (current.length !== original.length) {\n    return true\n  }\n  // Only sort when lengths match\n  const currentSorted = current.toSorted()\n  const originalSorted = original.toSorted()\n  for (let i = 0; i < currentSorted.length; i++) {\n    if (currentSorted[i] !== originalSorted[i]) {\n      return true\n    }\n  }\n  return false\n}\n```\n\nThis new approach is more efficient because:\n- It avoids the overhead of sorting and joining the arrays when lengths differ\n- It avoids consuming memory for the joined strings (especially important for large arrays)\n- It avoids mutating the original arrays\n- It returns early when a difference is found\n","frontend/.claude/skills/react-best-practices/rules/js-tosorted-immutable.md":"---\ntitle: Use toSorted() Instead of sort() for Immutability\nimpact: MEDIUM-HIGH\nimpactDescription: prevents mutation bugs in React state\ntags: javascript, arrays, immutability, react, state, mutation\n---\n\n## Use toSorted() Instead of sort() for Immutability\n\n`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.\n\n**Incorrect (mutates original array):**\n\n```typescript\nfunction UserList({ users }: { users: User[] }) {\n  // Mutates the users prop array!\n  const sorted = useMemo(\n    () => users.sort((a, b) => a.name.localeCompare(b.name)),\n    [users]\n  )\n  return <div>{sorted.map(renderUser)}</div>\n}\n```\n\n**Correct (creates new array):**\n\n```typescript\nfunction UserList({ users }: { users: User[] }) {\n  // Creates new sorted array, original unchanged\n  const sorted = useMemo(\n    () => users.toSorted((a, b) => a.name.localeCompare(b.name)),\n    [users]\n  )\n  return <div>{sorted.map(renderUser)}</div>\n}\n```\n\n**Why this matters in React:**\n\n1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only\n2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior\n\n**Browser support (fallback for older browsers):**\n\n`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:\n\n```typescript\n// Fallback for older browsers\nconst sorted = [...items].sort((a, b) => a.value - b.value)\n```\n\n**Other immutable array methods:**\n\n- `.toSorted()` - immutable sort\n- `.toReversed()` - immutable reverse\n- `.toSpliced()` - immutable splice\n- `.with()` - immutable element replacement\n","frontend/.claude/skills/react-best-practices/rules/rendering-activity.md":"---\ntitle: Use Activity Component for Show/Hide\nimpact: MEDIUM\nimpactDescription: preserves state/DOM\ntags: rendering, activity, visibility, state-preservation\n---\n\n## Use Activity Component for Show/Hide\n\nUse React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.\n\n**Usage:**\n\n```tsx\nimport { Activity } from 'react'\n\nfunction Dropdown({ isOpen }: Props) {\n  return (\n    <Activity mode={isOpen ? 'visible' : 'hidden'}>\n      <ExpensiveMenu />\n    </Activity>\n  )\n}\n```\n\nAvoids expensive re-renders and state loss.\n","frontend/.claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md":"---\ntitle: Animate SVG Wrapper Instead of SVG Element\nimpact: LOW\nimpactDescription: enables hardware acceleration\ntags: rendering, svg, css, animation, performance\n---\n\n## Animate SVG Wrapper Instead of SVG Element\n\nMany browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.\n\n**Incorrect (animating SVG directly - no hardware acceleration):**\n\n```tsx\nfunction LoadingSpinner() {\n  return (\n    <svg \n      className=\"animate-spin\"\n      width=\"24\" \n      height=\"24\" \n      viewBox=\"0 0 24 24\"\n    >\n      <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" />\n    </svg>\n  )\n}\n```\n\n**Correct (animating wrapper div - hardware accelerated):**\n\n```tsx\nfunction LoadingSpinner() {\n  return (\n    <div className=\"animate-spin\">\n      <svg \n        width=\"24\" \n        height=\"24\" \n        viewBox=\"0 0 24 24\"\n      >\n        <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" />\n      </svg>\n    </div>\n  )\n}\n```\n\nThis applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.\n","frontend/.claude/skills/react-best-practices/rules/rendering-conditional-render.md":"---\ntitle: Use Explicit Conditional Rendering\nimpact: LOW\nimpactDescription: prevents rendering 0 or NaN\ntags: rendering, conditional, jsx, falsy-values\n---\n\n## Use Explicit Conditional Rendering\n\nUse explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.\n\n**Incorrect (renders \"0\" when count is 0):**\n\n```tsx\nfunction Badge({ count }: { count: number }) {\n  return (\n    <div>\n      {count && <span className=\"badge\">{count}</span>}\n    </div>\n  )\n}\n\n// When count = 0, renders: <div>0</div>\n// When count = 5, renders: <div><span class=\"badge\">5</span></div>\n```\n\n**Correct (renders nothing when count is 0):**\n\n```tsx\nfunction Badge({ count }: { count: number }) {\n  return (\n    <div>\n      {count > 0 ? <span className=\"badge\">{count}</span> : null}\n    </div>\n  )\n}\n\n// When count = 0, renders: <div></div>\n// When count = 5, renders: <div><span class=\"badge\">5</span></div>\n```\n","frontend/.claude/skills/react-best-practices/rules/rendering-content-visibility.md":"---\ntitle: CSS content-visibility for Long Lists\nimpact: HIGH\nimpactDescription: faster initial render\ntags: rendering, css, content-visibility, long-lists\n---\n\n## CSS content-visibility for Long Lists\n\nApply `content-visibility: auto` to defer off-screen rendering.\n\n**CSS:**\n\n```css\n.message-item {\n  content-visibility: auto;\n  contain-intrinsic-size: 0 80px;\n}\n```\n\n**Example:**\n\n```tsx\nfunction MessageList({ messages }: { messages: Message[] }) {\n  return (\n    <div className=\"overflow-y-auto h-screen\">\n      {messages.map(msg => (\n        <div key={msg.id} className=\"message-item\">\n          <Avatar user={msg.author} />\n          <div>{msg.content}</div>\n        </div>\n      ))}\n    </div>\n  )\n}\n```\n\nFor 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).\n","frontend/.claude/skills/react-best-practices/rules/rendering-hoist-jsx.md":"---\ntitle: Hoist Static JSX Elements\nimpact: LOW\nimpactDescription: avoids re-creation\ntags: rendering, jsx, static, optimization\n---\n\n## Hoist Static JSX Elements\n\nExtract static JSX outside components to avoid re-creation.\n\n**Incorrect (recreates element every render):**\n\n```tsx\nfunction LoadingSkeleton() {\n  return <div className=\"animate-pulse h-20 bg-surface-strong\" />\n}\n\nfunction Container() {\n  return (\n    <div>\n      {loading && <LoadingSkeleton />}\n    </div>\n  )\n}\n```\n\n**Correct (reuses same element):**\n\n```tsx\nconst loadingSkeleton = (\n  <div className=\"animate-pulse h-20 bg-surface-strong\" />\n)\n\nfunction Container() {\n  return (\n    <div>\n      {loading && loadingSkeleton}\n    </div>\n  )\n}\n```\n\nThis is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.\n\n**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.\n","frontend/.claude/skills/react-best-practices/rules/rendering-svg-precision.md":"---\ntitle: Optimize SVG Precision\nimpact: LOW\nimpactDescription: reduces file size\ntags: rendering, svg, optimization, svgo\n---\n\n## Optimize SVG Precision\n\nReduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.\n\n**Incorrect (excessive precision):**\n\n```svg\n<path d=\"M 10.293847 20.847362 L 30.938472 40.192837\" />\n```\n\n**Correct (1 decimal place):**\n\n```svg\n<path d=\"M 10.3 20.8 L 30.9 40.2\" />\n```\n\n**Automate with SVGO:**\n\n```bash\nnpx svgo --precision=1 --multipass icon.svg\n```\n","frontend/.claude/skills/react-best-practices/rules/rerender-defer-reads.md":"---\ntitle: Defer State Reads to Usage Point\nimpact: MEDIUM\nimpactDescription: avoids unnecessary subscriptions\ntags: rerender, URLSearchParams, localStorage, optimization\n---\n\n## Defer State Reads to Usage Point\n\nDon't subscribe to dynamic state (URL params, localStorage) if you only read it inside callbacks.\n\n**Incorrect (subscribes to URL param changes, re-renders on every change):**\n\n```tsx\nfunction ShareButton({ chatId }: { chatId: string }) {\n  // Reading URL params at render time creates a subscription\n  const ref = new URLSearchParams(window.location.search).get('ref')\n\n  const handleShare = () => {\n    shareChat(chatId, { ref })\n  }\n\n  return <button onClick={handleShare}>Share</button>\n}\n```\n\n**Correct (reads on demand inside callback, no subscription):**\n\n```tsx\nfunction ShareButton({ chatId }: { chatId: string }) {\n  const handleShare = () => {\n    const params = new URLSearchParams(window.location.search)\n    const ref = params.get('ref')\n    shareChat(chatId, { ref })\n  }\n\n  return <button onClick={handleShare}>Share</button>\n}\n```\n\nSame principle applies to localStorage, sessionStorage, and cookies — read inside callbacks rather than at render time.\n","frontend/.claude/skills/react-best-practices/rules/rerender-dependencies.md":"---\ntitle: Narrow Effect Dependencies\nimpact: LOW\nimpactDescription: minimizes effect re-runs\ntags: rerender, useEffect, dependencies, optimization\n---\n\n## Narrow Effect Dependencies\n\nSpecify primitive dependencies instead of objects to minimize effect re-runs.\n\n**Incorrect (re-runs on any user field change):**\n\n```tsx\nuseEffect(() => {\n  console.log(user.id)\n}, [user])\n```\n\n**Correct (re-runs only when id changes):**\n\n```tsx\nuseEffect(() => {\n  console.log(user.id)\n}, [user.id])\n```\n\n**For derived state, compute outside effect:**\n\n```tsx\n// Incorrect: runs on width=767, 766, 765...\nuseEffect(() => {\n  if (width < 768) {\n    enableMobileMode()\n  }\n}, [width])\n\n// Correct: runs only on boolean transition\nconst isMobile = width < 768\nuseEffect(() => {\n  if (isMobile) {\n    enableMobileMode()\n  }\n}, [isMobile])\n```\n","frontend/.claude/skills/react-best-practices/rules/rerender-derived-state.md":"---\ntitle: Subscribe to Derived State\nimpact: MEDIUM\nimpactDescription: reduces re-render frequency\ntags: rerender, derived-state, media-query, optimization\n---\n\n## Subscribe to Derived State\n\nSubscribe to derived boolean state instead of continuous values to reduce re-render frequency.\n\n**Incorrect (re-renders on every pixel change):**\n\n```tsx\nfunction Sidebar() {\n  const width = useWindowWidth()  // updates continuously\n  const isMobile = width < 768\n  return <nav className={isMobile ? 'mobile' : 'desktop'} />\n}\n```\n\n**Correct (re-renders only when boolean changes):**\n\n```tsx\nfunction Sidebar() {\n  const isMobile = useMediaQuery('(max-width: 767px)')\n  return <nav className={isMobile ? 'mobile' : 'desktop'} />\n}\n```\n","frontend/.claude/skills/react-best-practices/rules/rerender-functional-setstate.md":"---\ntitle: Use Functional setState Updates\nimpact: MEDIUM\nimpactDescription: prevents stale closures and unnecessary callback recreations\ntags: react, hooks, useState, useCallback, callbacks, closures\n---\n\n## Use Functional setState Updates\n\nWhen updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.\n\n**Incorrect (requires state as dependency):**\n\n```tsx\nfunction TodoList() {\n  const [items, setItems] = useState(initialItems)\n  \n  // Callback must depend on items, recreated on every items change\n  const addItems = useCallback((newItems: Item[]) => {\n    setItems([...items, ...newItems])\n  }, [items])  // ❌ items dependency causes recreations\n  \n  // Risk of stale closure if dependency is forgotten\n  const removeItem = useCallback((id: string) => {\n    setItems(items.filter(item => item.id !== id))\n  }, [])  // ❌ Missing items dependency - will use stale items!\n  \n  return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />\n}\n```\n\nThe first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.\n\n**Correct (stable callbacks, no stale closures):**\n\n```tsx\nfunction TodoList() {\n  const [items, setItems] = useState(initialItems)\n  \n  // Stable callback, never recreated\n  const addItems = useCallback((newItems: Item[]) => {\n    setItems(curr => [...curr, ...newItems])\n  }, [])  // ✅ No dependencies needed\n  \n  // Always uses latest state, no stale closure risk\n  const removeItem = useCallback((id: string) => {\n    setItems(curr => curr.filter(item => item.id !== id))\n  }, [])  // ✅ Safe and stable\n  \n  return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />\n}\n```\n\n**Benefits:**\n\n1. **Stable callback references** - Callbacks don't need to be recreated when state changes\n2. **No stale closures** - Always operates on the latest state value\n3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks\n4. **Prevents bugs** - Eliminates the most common source of React closure bugs\n\n**When to use functional updates:**\n\n- Any setState that depends on the current state value\n- Inside useCallback/useMemo when state is needed\n- Event handlers that reference state\n- Async operations that update state\n\n**When direct updates are fine:**\n\n- Setting state to a static value: `setCount(0)`\n- Setting state from props/arguments only: `setName(newName)`\n- State doesn't depend on previous value\n\n**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.\n","frontend/.claude/skills/react-best-practices/rules/rerender-lazy-state-init.md":"---\ntitle: Use Lazy State Initialization\nimpact: MEDIUM\nimpactDescription: wasted computation on every render\ntags: react, hooks, useState, performance, initialization\n---\n\n## Use Lazy State Initialization\n\nPass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.\n\n**Incorrect (runs on every render):**\n\n```tsx\nfunction FilteredList({ items }: { items: Item[] }) {\n  // buildSearchIndex() runs on EVERY render, even after initialization\n  const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))\n  const [query, setQuery] = useState('')\n  \n  // When query changes, buildSearchIndex runs again unnecessarily\n  return <SearchResults index={searchIndex} query={query} />\n}\n\nfunction UserProfile() {\n  // JSON.parse runs on every render\n  const [settings, setSettings] = useState(\n    JSON.parse(localStorage.getItem('settings') || '{}')\n  )\n  \n  return <SettingsForm settings={settings} onChange={setSettings} />\n}\n```\n\n**Correct (runs only once):**\n\n```tsx\nfunction FilteredList({ items }: { items: Item[] }) {\n  // buildSearchIndex() runs ONLY on initial render\n  const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))\n  const [query, setQuery] = useState('')\n  \n  return <SearchResults index={searchIndex} query={query} />\n}\n\nfunction UserProfile() {\n  // JSON.parse runs only on initial render\n  const [settings, setSettings] = useState(() => {\n    const stored = localStorage.getItem('settings')\n    return stored ? JSON.parse(stored) : {}\n  })\n  \n  return <SettingsForm settings={settings} onChange={setSettings} />\n}\n```\n\nUse lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.\n\nFor simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.\n","frontend/.claude/skills/react-best-practices/rules/rerender-memo.md":"---\ntitle: Extract to Memoized Components\nimpact: MEDIUM\nimpactDescription: enables early returns\ntags: rerender, memo, useMemo, optimization\n---\n\n## Extract to Memoized Components\n\nExtract expensive work into memoized components to enable early returns before computation.\n\n**Incorrect (computes avatar even when loading):**\n\n```tsx\nfunction Profile({ user, loading }: Props) {\n  const avatar = useMemo(() => {\n    const id = computeAvatarId(user)\n    return <Avatar id={id} />\n  }, [user])\n\n  if (loading) return <Skeleton />\n  return <div>{avatar}</div>\n}\n```\n\n**Correct (skips computation when loading):**\n\n```tsx\nconst UserAvatar = memo(function UserAvatar({ user }: { user: User }) {\n  const id = useMemo(() => computeAvatarId(user), [user])\n  return <Avatar id={id} />\n})\n\nfunction Profile({ user, loading }: Props) {\n  if (loading) return <Skeleton />\n  return (\n    <div>\n      <UserAvatar user={user} />\n    </div>\n  )\n}\n```\n\n**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.\n","frontend/.claude/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Freact-best-practices%2Frules%2Frerender-simple-expression-in-memo.md -->","frontend/.claude/skills/react-best-practices/rules/rerender-transitions.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Freact-best-practices%2Frules%2Frerender-transitions.md -->","frontend/.claude/skills/react-best-practices/rules/use-effect.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Freact-best-practices%2Frules%2Fuse-effect.md -->","frontend/.claude/skills/security-scan/SKILL.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fsecurity-scan%2FSKILL.md -->","frontend/.claude/skills/security-scan/rules/_sections.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fsecurity-scan%2Frules%2F_sections.md -->","frontend/.claude/skills/security-scan/rules/_template.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fsecurity-scan%2Frules%2F_template.md -->","frontend/.claude/skills/security-scan/rules/vuln-direct-deps.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fsecurity-scan%2Frules%2Fvuln-direct-deps.md -->","frontend/.claude/skills/security-scan/rules/vuln-transitive-deps.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fsecurity-scan%2Frules%2Fvuln-transitive-deps.md -->","frontend/.claude/skills/state-management/SKILL.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fstate-management%2FSKILL.md -->","frontend/.claude/skills/state-management/rules/use-react-query-for-server.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fstate-management%2Frules%2Fuse-react-query-for-server.md -->","frontend/.claude/skills/state-management/rules/use-zustand-persist.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fstate-management%2Frules%2Fuse-zustand-persist.md -->","frontend/.claude/skills/state-management/rules/use-zustand-selectors.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fstate-management%2Frules%2Fuse-zustand-selectors.md -->","frontend/.claude/skills/tanstack-router-migration/SKILL.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftanstack-router-migration%2FSKILL.md -->","frontend/.claude/skills/tanstack-router-migration/references/build-configuration.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftanstack-router-migration%2Freferences%2Fbuild-configuration.md -->","frontend/.claude/skills/tanstack-router-migration/references/incremental-migration.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftanstack-router-migration%2Freferences%2Fincremental-migration.md -->","frontend/.claude/skills/tanstack-router-migration/references/migration-patterns.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftanstack-router-migration%2Freferences%2Fmigration-patterns.md -->","frontend/.claude/skills/tanstack-router-migration/references/route-templates.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftanstack-router-migration%2Freferences%2Froute-templates.md -->","frontend/.claude/skills/testing/SKILL.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2FSKILL.md -->","frontend/.claude/skills/testing/rules/mock-transport.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2Frules%2Fmock-transport.md -->","frontend/.claude/skills/testing/rules/no-ui-rendering-tests.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2Frules%2Fno-ui-rendering-tests.md -->","frontend/.claude/skills/testing/rules/test-mock-patterns.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2Frules%2Ftest-mock-patterns.md -->","frontend/.claude/skills/testing/rules/test-unit-vs-integration.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2Frules%2Ftest-unit-vs-integration.md -->","frontend/.claude/skills/testing/rules/test-zustand-stores.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2Frules%2Ftest-zustand-stores.md -->","frontend/.claude/skills/ui-development/SKILL.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2FSKILL.md -->","frontend/.claude/skills/ui-development/rules/icon-system.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Ficon-system.md -->","frontend/.claude/skills/ui-development/rules/registry-upgrade.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Fregistry-upgrade.md -->","frontend/.claude/skills/ui-development/rules/style-no-margin-on-registry.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Fstyle-no-margin-on-registry.md -->","frontend/.claude/skills/ui-development/rules/style-use-tokens.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Fstyle-use-tokens.md -->","frontend/.claude/skills/ui-development/rules/style-use-variants.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Fstyle-use-variants.md -->","frontend/.claude/skills/ui-development/rules/use-ui-registry.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Fuse-ui-registry.md -->","frontend/.claude/skills/web-design-guidelines/SKILL.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fweb-design-guidelines%2FSKILL.md -->","frontend/AGENTS.md":"# Redpanda Console Frontend\n\nReact 19 · Bun · Rsbuild\n\n## Critical Rules\n\n- New code MUST use Registry components (`src/components/redpanda-ui/`), react-hook-form + Zod, Bun unit tests, Rstest integration/browser/E2E tests, and Connect Query\n- See [no-legacy](.claude/skills/code-standards/rules/no-legacy.md) for prohibited patterns\n- `src/protogen/` is generated — DO NOT EDIT\n\n## Commands\n\nAlways use `bun run <script>` for project tools. Never use `bunx`, `npx`, or run tools directly.\n\n| Command | Purpose |\n|---------|---------|\n| `bun start` | Dev server :3000 |\n| `bun run build` | Production build |\n| `bun run test` | Unit, integration, federation and browser checks |\n| `bun run test:unit` | `.test.ts` (Bun) |\n| `bun run test:integration` | `.test.tsx` (happy-dom) |\n| `bun run lint` | Biome/Ultracite linter |\n| `bun run type:check` | TypeScript |\n\n## Verify Changes\n\nAfter changes: `bun run type:check && bun run lint && bun run test`\n\nClassify tests by behavior before choosing filenames. See [test guide](tests/README.md) for full-stack variants, fixtures, coverage and CI.\n\n## Directory Structure\n\n```\nsrc/\n├── components/\n│   ├── pages/          # Feature pages\n│   └── redpanda-ui/    # UI Registry (USE THIS)\n├── react-query/        # Connect Query hooks\n├── hooks/              # Custom hooks\n├── utils/              # Utilities\n└── protogen/           # Generated (DO NOT EDIT)\n```\n\n## Plan Mode\n\n- Extremely concise plans. Sacrifice grammar for concision.\n- End each plan with unresolved questions, if any.\n\n## Compaction\n\nWhen compacting, preserve: modified file list, test commands, and error messages.\n","frontend/CLAUDE.md":"@AGENTS.md\n",".cursorrules (kubestellar-console.mdc)":"---\ndescription: \"KubeStellar Console — Multi-cluster Kubernetes dashboard development rules\"\nglobs: **/*.tsx, **/*.ts, **/*.go, web/src/**/*.ts, web/src/**/*.tsx, pkg/**/*.go, cmd/**/*.go\nalwaysApply: false\n---\n# KubeStellar Console Development Rules\n\n## Project Structure\n- Frontend: React + TypeScript in `/web/`\n- Backend: Go (Fiber v2) in root directory\n- Build: `cd web && npm run build && npm run lint` before every commit\n- Cards: Dashboard card components in `web/src/components/cards/`\n- Hooks: Data fetching hooks in `web/src/hooks/`\n\n## Card Development\n- All data fetching MUST go through `useCache`/`useCached*` hooks\n- Always destructure and pass `isDemoData` and `isRefreshing` to `useCardLoadingState()`\n- Never use demo data during loading: `isDemoFallback && !isLoading`\n- Hook ordering: `useCardLoadingState` AFTER hooks providing `isDemoData`\n\n## Array Safety\n- NEVER call `.join()`, `.map()`, `.filter()`, `.forEach()`, `for...of` on values that might be undefined\n- Always guard: `(data || []).map(...)` or `(data || []).join(', ')`\n\n## No Magic Numbers\n- Numeric literals should be named constants, except trivial literals (`0`, `1`, `-1`) in clear local contexts\n- Use constants from `lib/constants/` (time.ts, network.ts, ui.ts)\n\n## Styling\n- Tailwind CSS with `cn()` utility for merging classNames\n- Never use raw hex colors — use semantic Tailwind classes (`text-foreground`, `bg-primary`, `bg-card`)\n- Status colors: `text-green-400`/`bg-green-500/10` (success), `text-yellow-400`/`bg-yellow-500/10` (warning), `text-red-400`/`bg-red-500/10` (error), `text-cyan-400`/`bg-cyan-500/10` (info) — these map to the design system's semantic status tokens and are the only permitted palette classes\n\n## Internationalization\n- All user-facing strings use `t()` from `react-i18next`\n- Keys in `web/src/locales/en/` JSON files\n- Never use raw strings for UI text\n\n## Go Backend\n- Fiber v2 handlers: `func(c *fiber.Ctx) error`\n- Use `fiber.NewError(statusCode, message)` for errors\n- Always `make([]T, 0)` not `var x []T` (nil → null in JSON)\n- Use `log/slog` for structured logging\n- Multi-cluster queries use goroutines + sync.WaitGroup\n\n## Cluster Deduplication\n- Always use `DeduplicatedClusters()` when iterating clusters\n- Multiple kubeconfig contexts can point to same physical cluster\n"},"files":{".claude/commands/changelog/update.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=.claude%2Fcommands%2Fchangelog%2Fupdate.md -->",".claude/commands/fix-snyk-vulnerabilities.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=.claude%2Fcommands%2Ffix-snyk-vulnerabilities.md -->",".github/copilot-instructions.md":"# Copilot instructions: Redpanda Console\n\nGo backend (`backend/`) + React 19 SPA (`frontend/`) sharing a protobuf contract (`proto/`).\nTask drives builds from the repository root.\n\n## Where the rules live\n\nArea rules are **not** repeated here. They live in `AGENTS.md` files and the nearest one to a\nchanged file wins:\n\n| Path | Governing file |\n|---|---|\n| `backend/**` | `backend/AGENTS.md` |\n| `frontend/**` | `frontend/AGENTS.md` |\n| `proto/**`, root config, `taskfiles/**` | `AGENTS.md` |\n| commits, PR description, review conduct | this file |\n\nWhen a review comment enforces an area rule, name the file it came from so the author can\nread the rule in context.\n\n## Commit messages\n\nFormat is `area[/detail]: short description`.\n\nexample:\n```\nbackend: resolve schema IDs in the topic's registry context\n\nBefore this change the message viewer always looked schemas up in the\ndefault context, so a topic bound to a named context returned a decode\nerror. Resolution now takes the topic's context, which also removes the\nspecial case in the sample handler.\n```\n\n- Subject: 50 characters or fewer, lowercase after the prefix, no trailing period.\n- Common areas: `backend`, `frontend`, `proto`, `topics`, `chore`, `deps`.\n- Body: wrap at 72 columns. Explain **why**, not what: the state being moved away from, why\n  it is no longer wanted, and how the new state addresses that. Then any non-obvious\n  technical detail.\n- A commit message must stand alone. No \"in the previous commit\", no references to meetings\n  or threads. That context goes in the PR description.\n- One logical change per commit, and every commit must build on its own so `git bisect`\n  works.\n\nParts of the history use Conventional Commits (`feat(frontend): ...`). `area:` is the\ndocumented standard in `CONTRIBUTING.md` and the form to use for new work.\n\n## Pull requests\n\n- Title follows the same `area: description` convention.\n- The description carries the high-level narrative that individual commits must not depend\n  on, plus examples of what's being added.\n- Fold review fixups back into the commits they modify before merge. Do not leave\n  \"address review feedback\" commits in the final history.\n- `git rebase --keep-base` keeps the diff for reviewers minimal on a force-push.\n- Call out explicitly when a change touches `backend/pkg/api/hooks.go` or `proto/`. Those are\n  the contract surfaces `console-enterprise` builds on, and a signature change there breaks\n  it. (`backend/pkg/api/hooks/` is a stale duplicate that nothing imports.)\n\n## Comments\n\nRepo-wide: keep comments to a minimum. Less is more. Full rules in\n[backend/AGENTS.md](../backend/AGENTS.md#comments).\n\n- Comment the **why**, never the what. A non-obvious constraint, a protocol quirk, a\n  deliberate deviation. Not `// increment the offset` above `offset++`.\n- Doc comments on exported Go identifiers are mandatory (`revive` runs with all rules on) and\n  are the exception to this, not a license to annotate everything.\n- No banner comments, no commented-out code, no comments that exist to describe what the diff\n  changed. That belongs in the commit message.\n- `TODO`/`FIXME` must name a ticket or an owner.\n- Prefer a better name or a small extracted function over a paragraph explaining the code.\n\nWhen reviewing, flag added comments that restate the code and suggest deleting them. This is\nthe most common form of bloat in AI-assisted pull requests.\n\n## Review priorities\n\nIn order:\n\n1. **Correctness.** Wrong behavior, unhandled errors, swallowed errors, nil dereferences,\n   races, resource leaks, missing `ctx` propagation.\n2. **The area rules** in the relevant `AGENTS.md`, especially the \"Hard rules\" section of\n   `backend/AGENTS.md` and the \"Critical Rules\" of `frontend/AGENTS.md`.\n3. **Contract and compatibility.** Proto changes that are not wire-compatible, hook signature\n   changes, API version placement, generated output not regenerated.\n4. **Tests.** New behavior needs a test. Backend prefers table-driven with `t.Run`; frontend\n   splits `.test.ts` (unit, node) from `.test.tsx` (integration, happy-dom).\n5. **Noise.** Comments that restate the code, commented-out code, and dead scaffolding. See\n   Comments above.\n\n## Do not flag\n\n- Anything under a generated or vendored path: `backend/pkg/protogen/`,\n  `frontend/src/protogen/`, `frontend/src/routeTree.gen.ts`,\n  `frontend/src/components/redpanda-ui/`, `build/`. Changes there are either generated output\n  or upstream registry content.\n- Formatting and style nits already enforced by tooling. `golangci-lint` 2.10 (37 linters,\n  `revive` with all rules on, `sloglint`, `gci`) and Biome/Ultracite gate those in CI, and a\n  duplicate review comment adds noise without adding information.\n- Line length, import order, or trailing whitespace. The formatters own those.\n\n## What CI enforces\n\n| Workflow | Gate |\n|---|---|\n| `backend-lint-test.yml` | `golangci-lint` + unit and integration tests |\n| `frontend-verify.yml` | Biome/Ultracite (with a dirty-tree check), `ultracite doctor`, type check, build, tests |\n| `buf.yml` | proto lint, format, and breaking-change detection |\n| `proto-generate.yml` | fails if generated proto output is not committed |\n| `frontend-react-doctor.yml` | React Doctor on frontend changes |\n| `frontend-ui-audit.yml` | advisory only: registry drift, off-token colors, ad-hoc utility classes |\n| `repository-dispatch.yml` | triggers the `console-enterprise` build after verify |\n\nPrefer pointing at the gate that will catch an issue over restating what the gate already\nsays.\n","AGENTS.md":"# Redpanda Console\n\nGo backend (`backend/`) + React 19 single-page app (`frontend/`) sharing a protobuf API\ncontract (`proto/`). Builds and tooling are driven by [Task](https://taskfile.dev/) from the\nrepository root.\n\n## Which rules apply where\n\nRead the nearest file to what you are changing. The nested files own their area; this file\nonly routes.\n\n| You are changing | Read |\n|---|---|\n| `backend/**` | [backend/AGENTS.md](backend/AGENTS.md) |\n| `frontend/**` | [frontend/AGENTS.md](frontend/AGENTS.md) |\n| `proto/**` | this file, plus `buf.yaml` and `taskfiles/proto.yaml` |\n| root config, `taskfiles/**`, `.github/**` | this file |\n| commit messages, PR description, review | [.github/copilot-instructions.md](.github/copilot-instructions.md) and [CONTRIBUTING.md](CONTRIBUTING.md) |\n\n## Commands\n\nBackend and proto commands run **from the repository root** via `task`. The `./taskw` wrapper\npins the Go toolchain and helper binaries under `build/`.\n\n```bash\ntask backend:fmt            # format\ntask backend:lint           # golangci-lint\ntask backend:test-unit      # unit tests\ntask backend:test-integration   # unit + integration (needs Docker)\ntask backend:verify         # lint + generate + integration tests\ntask proto:lint             # buf lint\ntask proto:generate         # regenerate backend + frontend protogen\n```\n\nFrontend commands run **from `frontend/`** via `bun run <script>`.\n\n## Never edit (generated or vendored)\n\n- `backend/pkg/protogen/` and `frontend/src/protogen/` (regenerate with `task proto:generate`)\n- `frontend/src/routeTree.gen.ts` (TanStack Router)\n- `frontend/src/components/redpanda-ui/` (UI Registry, sourced from `redpanda-data/ui-registry`)\n- `build/` (pinned toolchain)\n\n## Enterprise boundary\n\nThis repository is the **OSS foundation**. `console-enterprise` imports\n`github.com/redpanda-data/console/backend` and extends it through the hooks system in\n`backend/pkg/api/hooks.go`, which lets it add interceptors, change routes, and replace service\nimplementations.\n\nFeatures that live behind those hooks (authentication, RBAC, secret management, shadow links,\npipelines, debug bundles) **cannot be exercised from this repository alone**. To test an OSS\nchange against enterprise, add a replace directive in `console-enterprise/backend/go.mod`:\n\n```go\nreplace github.com/redpanda-data/console/backend => /path/to/console/backend\n```\n\n## Proto changes\n\n`proto/` is published to the Buf Schema Registry as `buf.build/redpandadata/dataplane`. A\nchange here is an API contract change:\n\n1. Edit the `.proto` file.\n2. `task proto:lint && task proto:format`\n3. `task proto:generate` and commit the generated output. `proto-generate.yml` fails the PR if\n   generated code is out of date.\n4. Breaking-change detection runs in CI. Some alpha packages are exempt; check the `ignore`\n   list in `buf.yaml` before assuming yours is. Everything else is wire-compatible-only.\n","CLAUDE.md":"@AGENTS.md\n","backend/AGENTS.md":"# Redpanda Console Backend\n\nGo HTTP API server for managing and debugging Kafka/Redpanda workloads. Serves a legacy REST\nAPI under `/api` and a Connect RPC API re-exposed as JSON through gRPC-Gateway under\nversioned paths.\n\nModule: `github.com/redpanda-data/console/backend`. See [../AGENTS.md](../AGENTS.md) for\nrepo-wide rules.\n\n**How to use this file.** It tells you the rules you cannot infer and where to look for the\nrest. It is not a specification. Before relying on a specific, read the source: the linter\nconfig for lint rules, the Taskfile for commands, `routes.go` for what is wired, and the\npackage you are editing for its own patterns. When this file and the code disagree, the code\nwins and this file needs a fix.\n\n## Hard rules\n\nNon-obvious and enforced. Everything else the linter will tell you.\n\n**Errors.** Construct Connect errors through `apierrors`\n([pkg/api/connect/errors/](pkg/api/connect/errors/)), never `connect.NewError` directly, or\nyou lose the error details the helper always attaches. That package also holds converters for\nKafka, Schema Registry, and Redpanda Admin API errors; check for one before writing your own.\nLegacy REST handlers use a different model, `*rest.Error` with `rest.SendRESTError`.\n\n**Logging.** `log/slog` only, and `sloglint` is strict:\n\n- No global logger. Take a `*slog.Logger` as a constructor parameter and store it.\n- Attributes only: `slog.String(\"topic_name\", name)`, never loose key/value pairs.\n- snake_case keys, lowercase messages, and `\"error\"` rather than `\"err\"`.\n- Sub-loggers via `loggerpkg.Named(parent, \"topic_service\")`.\n\n**Doc comments.** `revive` runs with `enable-all-rules: true`, so every exported identifier\nneeds a doc comment.\n\n**License header.** Every new `.go` file starts with the BSL block:\n\n```go\n// Copyright 2026 Redpanda Data, Inc.\n//\n// Use of this software is governed by the Business Source License\n// included in the file licenses/BSL.md\n//\n// As of the Change Date specified in that file, in accordance with\n// the Business Source License, use of this software will be governed\n// by the Apache License, Version 2.0\n```\n\n**Imports.** Three blocks: standard library, third-party, then\n`github.com/redpanda-data/console/backend`. `task backend:fmt` sorts them.\n\n**Generated code.** Never hand-edit `pkg/protogen/`. Run `task proto:generate` from the\nrepository root.\n\nComplexity limits are enforced too, but you will hit those as lint failures rather than\nneeding to plan for them. The full linter set is in [.golangci.yaml](.golangci.yaml).\n\n## Comments\n\nKeep comments to a minimum. Less is more.\n\n- Doc comments on exported identifiers are required, as above. One line is enough.\n- Do not restate the code. `// increment the offset` above `offset++` is noise.\n- No banner comments (`// --- helpers ---`), no labelling the obvious.\n- Comment the **why**: a non-obvious constraint, a protocol quirk, a deliberate deviation, a\n  reason an ordering matters. Link the ticket or upstream issue if there is one.\n- Prefer a better name or a small extracted function over a paragraph of explanation.\n- No commented-out code. `TODO`/`FIXME` must name a ticket or an owner.\n\nDo not add comments to code you touched just to show what changed. That goes in the commit\nmessage.\n\n## Commands\n\nFrom the **repository root**. `task --list` has the rest.\n\n```bash\ntask backend:fmt                # format\ntask backend:lint               # golangci-lint\ntask backend:test-unit          # unit tests\ntask backend:test-integration   # unit + integration, needs Docker\ntask backend:verify             # lint, generate, integration tests\ntask backend:generate           # go generate, proto, format\n```\n\n`CLI_ARGS` is forwarded: `task backend:test-integration -- ./pkg/api/...`.\n\nTo invoke `go` directly, run it **from `backend/`**, since the commands are module-relative:\n\n```bash\ngo test ./pkg/serde/... -run TestJsonSerde\ngo test --tags=integration ./pkg/api/connect/integration/... -run TestSuite/TestTopicV1\n```\n\nRun the server from `backend/`, with a local cluster from `docs/local/`\n(`docker compose up -d`):\n\n```bash\nKAFKA_BROKERS=localhost:9092 SERVEFRONTEND=false SERVER_LISTENPORT=9090 go run cmd/api/main.go\n```\n\n**Before pushing:** `task backend:fmt && task backend:lint && task backend:test-unit`, then\n`task backend:test-integration` if you touched `pkg/api/` or `pkg/console/`.\n\n## Where things are\n\nOnly `cmd/` and `pkg/`. No `internal/`.\n\n| Path | Holds |\n|---|---|\n| `cmd/api/` | the single binary |\n| `pkg/api/` | HTTP and gRPC layer. `api.go` wires dependencies, `routes.go` mounts everything, `hooks.go` is the enterprise extension contract |\n| `pkg/api/connect/service/<domain>/<version>/` | Connect service implementations |\n| `pkg/console/` | core business logic behind the `Servicer` interface |\n| `pkg/factory/` | Kafka, Redpanda Admin, and Schema Registry client factories, consumed as interfaces |\n| `pkg/serde/` | auto-detecting serialization for Kafka messages |\n| `pkg/config/` | koanf config, one file per sub-struct |\n| `pkg/logger/` | slog setup with functional options |\n| `pkg/testutil/` | testcontainers helpers; image tags come from `test-images.json` at the repo root |\n| `pkg/protogen/` | generated, do not edit |\n\n`routes.go` is the map. It shows which services exist, which API versions are mounted, and\nwhich are OSS placeholders that enterprise replaces. Read it rather than assuming.\n\n## Conventions\n\n- Wrap errors with context: `fmt.Errorf(\"failed to describe topic: %w\", err)`. Compare with\n  `errors.Is`/`errors.As`, never `==` or string matching.\n- `ctx context.Context` is the first parameter, and never stored in a struct.\n- Accept interfaces, return concrete types. Assert conformance at package level:\n  `var _ dataplanev1connect.TopicServiceHandler = (*Service)(nil)`.\n- Constructors are `NewX(cfg, logger, collaborators...)`, dropping any the service does not\n  need. No setters.\n- A service package splits by role: `service.go` for the RPCs, `mapper.go` for proto\n  conversion, `defaulter.go` for request defaults, `util.go` for error translation. The\n  package is named for the domain even inside a `v1/` directory, so imports are aliased\n  (`topicsvcv1`).\n- `pkg/console` puts one exported operation per file, named in snake_case after the operation.\n- Config sub-structs implement `SetDefaults`, `RegisterFlags`, and `Validate` as needed. Do\n  not add an empty method to complete the set. Read `LoadConfig` in\n  [pkg/config/config.go](pkg/config/config.go) before changing precedence; the order of\n  defaults, flags, file, and env is load-bearing and not what you would guess.\n- Wiring is manual, no DI framework.\n\n## Enterprise boundary\n\nThis is the OSS foundation. `console-enterprise` imports it and extends it through the `Hooks`\ncontract in [pkg/api/hooks.go](pkg/api/hooks.go), which lets it add interceptors, replace OSS\nservice implementations, register additional services, and supply license and feature-flag\ninformation.\n\nChanging a hook signature breaks console-enterprise. Call it out in the PR description.\n\nThere is a stale duplicate of these declarations in `pkg/api/hooks/`. Nothing imports it;\ndo not add to it.\n\n## Testing\n\n- Table-driven is the house pattern: a slice of cases with a `name` and a `validateFn`\n  closure, run through `t.Run(tc.name, ...)`. See\n  [topic/v1/mapper_test.go](pkg/api/connect/service/topic/v1/mapper_test.go).\n- testify: `require` where a failure makes later assertions meaningless, `assert` for value\n  comparisons. Both in one test is normal.\n- Tests live in the same package as the code (`package topic`, not `topic_test`).\n- Unit tests carry no build tag. Integration tests carry `//go:build integration` and the\n  `_integration_test.go` suffix, except in `pkg/api/connect/integration/` where the tag alone\n  marks them.\n- Integration suites use testcontainers-go and testify/suite. See\n  [integration/api_suite_test.go](pkg/api/connect/integration/api_suite_test.go).\n- Mocks are gomock, from a `//go:generate mockgen` directive next to the interface.\n  Regenerate with `task backend:generate`.\n","backend/CLAUDE.md":"@AGENTS.md\n","frontend/.claude/agents/playwright-test-generator.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fagents%2Fplaywright-test-generator.md -->","frontend/.claude/agents/playwright-test-healer.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fagents%2Fplaywright-test-healer.md -->","frontend/.claude/agents/playwright-test-planner.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fagents%2Fplaywright-test-planner.md -->","frontend/.claude/agents/redpanda-logo-fetcher.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fagents%2Fredpanda-logo-fetcher.md -->","frontend/.claude/rules/test-unit-vs-integration.md":"---\npaths:\n  - \"src/**/*.test.{ts,tsx}\"\n---\n\n# Test classification\n\nClassify what the test exercises before choosing its filename:\n\n- Pure logic/mocked module contracts: `.test.ts`, Bun (`bun run test:unit`).\n- React, DOM, hooks, state/transport integration: `.test.tsx`, Rstest + Happy DOM.\n- Configuration/subprocess integration: `.integration.test.ts`, Rstest.\n- Native browser and full running-app journeys: `.spec.ts`, Rstest + Playwright.\n\nSee [test guide](../../tests/README.md) for commands and fixtures. Keep full-stack journeys on Testcontainers; mocked bundle tests do not replace them.\n","frontend/.claude/skills/api-patterns/SKILL.md":"---\nname: api-patterns\ndescription: Connect Query patterns for API calls. Use when working with mutations, queries, or data fetching.\n---\n\n# API Patterns\n\nMake API calls with Connect Query and handle responses properly.\n\n## Activation Conditions\n\n- Making API calls\n- Using Connect Query hooks\n- Cache invalidation\n- Mutations and optimistic updates\n- Toast notifications for errors\n\n## Quick Reference\n\n| Action | Rule |\n|--------|------|\n| Fetch data | `use-connect-query.md` |\n| After mutation | `api-invalidate-cache.md` |\n| Handle errors | `api-toast-errors.md` (use `formatToastErrorMessage` in onError) |\n| Protobuf files | `protobuf-no-edit.md` |\n\n## Key Locations\n\n| Location | Purpose |\n|----------|---------|\n| `/src/react-query/` | Connect Query hooks |\n| `/src/protogen/` | Generated protos (DO NOT EDIT) |\n\nRegenerate protos: `task proto:generate` (from repo root)\n\n## Rules\n\nSee `rules/` directory for detailed guidance on queries, mutations, cache invalidation, and error handling.\n","frontend/.claude/skills/api-patterns/rules/api-invalidate-cache.md":"---\ntitle: Invalidate Cache After Mutations\nimpact: HIGH\nimpactDescription: Stale cache after mutations causes UI to show outdated data\ntags: cache, invalidation, mutation, react-query\n---\n\n# Invalidate Cache After Mutations (HIGH)\n\n## Explanation\n\nAfter mutations (create, update, delete), invalidate related queries to ensure the UI reflects the latest server state. Without invalidation, users see stale data until they manually refresh.\n\n## Incorrect\n\n```tsx\n// No invalidation after mutation\nconst mutation = useMutation(updateCluster, {\n  onSuccess: () => {\n    toast.success('Cluster updated');\n    // UI still shows old data!\n  },\n});\n```\n\n```tsx\n// Invalidating wrong query key\nconst mutation = useMutation(updateCluster, {\n  onSuccess: () => {\n    queryClient.invalidateQueries({ queryKey: ['clusters'] }); // Wrong key\n  },\n});\n```\n\n## Correct\n\n```tsx\nimport { useMutation } from '@connectrpc/connect-query';\nimport { useQueryClient } from '@tanstack/react-query';\n\nconst queryClient = useQueryClient();\n\nconst mutation = useMutation(updateCluster, {\n  onSuccess: () => {\n    // Invalidate specific query\n    queryClient.invalidateQueries({\n      queryKey: ['getCluster', clusterId],\n    });\n\n    // Or invalidate all cluster queries\n    queryClient.invalidateQueries({\n      queryKey: ['getClusters'],\n    });\n\n    toast.success('Cluster updated');\n  },\n});\n```\n\n## Optimistic Updates\n\nFor instant UI feedback:\n\n```tsx\nconst mutation = useMutation(updateCluster, {\n  onMutate: async (newData) => {\n    // Cancel outgoing refetches\n    await queryClient.cancelQueries({ queryKey: ['getCluster', id] });\n\n    // Snapshot previous value\n    const previous = queryClient.getQueryData(['getCluster', id]);\n\n    // Optimistically update\n    queryClient.setQueryData(['getCluster', id], newData);\n\n    return { previous };\n  },\n  onError: (err, newData, context) => {\n    // Rollback on error\n    queryClient.setQueryData(['getCluster', id], context?.previous);\n  },\n  onSettled: () => {\n    // Always refetch after mutation settles\n    queryClient.invalidateQueries({ queryKey: ['getCluster', id] });\n  },\n});\n```\n\n## Reference\n\n- [Query Invalidation](https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation)\n- [Optimistic Updates](https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates)\n","frontend/.claude/skills/api-patterns/rules/api-toast-errors.md":"---\ntitle: Toast Error Messages\nimpact: HIGH\nimpactDescription: Consistent error messages help users understand and recover from failures\ntags: error-handling, toast, ux, mutations\n---\n\n# Toast Error Messages (HIGH)\n\n## Explanation\n\nUse `formatToastErrorMessage` utility for consistent, user-friendly error messages in mutation `onError` handlers. This provides a standard format that includes the action, entity, and error details.\n\n## Incorrect\n\n```tsx\n// Generic error message\nconst mutation = useMutation(deleteCluster, {\n  onError: () => {\n    toast.error('Something went wrong');\n  },\n});\n\n// Raw error object\nconst mutation = useMutation(deleteCluster, {\n  onError: (error) => {\n    toast.error(error.message); // May be technical/unhelpful\n  },\n});\n\n// No error handling\nconst mutation = useMutation(deleteCluster, {\n  onSuccess: () => toast.success('Deleted'),\n  // Missing onError!\n});\n```\n\n## Correct\n\n```tsx\nimport { formatToastErrorMessage } from 'utils/toast.utils';\n\nconst mutation = useMutation(deleteCluster, {\n  onSuccess: () => {\n    toast.success('Cluster deleted');\n  },\n  onError: (error) => {\n    toast.error(formatToastErrorMessage({\n      error: {\n        reason: error.message,\n        internalCode: error.code,\n      },\n      action: 'delete',\n      entity: 'cluster',\n    }));\n  },\n});\n\n// Output: \"Failed to delete cluster due to: reason (code: 123)\"\n```\n\n## Standard Actions\n\n| Action | Use For |\n|--------|---------|\n| `create` | POST / create operations |\n| `update` | PUT / PATCH operations |\n| `delete` | DELETE operations |\n| `fetch` | GET operations (rare) |\n\n## Error Boundaries for Render Errors\n\n```tsx\nimport * as Sentry from '@sentry/react';\n\n<Sentry.ErrorBoundary fallback={<ErrorFallback />}>\n  <ClusterDashboard />\n</Sentry.ErrorBoundary>\n```\n\n## Reference\n\n- `src/utils/toast.utils.ts` - Toast utility implementation\n","frontend/.claude/skills/api-patterns/rules/protobuf-no-edit.md":"---\ntitle: Protobuf No Edit\nimpact: CRITICAL\nimpactDescription: Never edit files in protogen/ - they are auto-generated\ntags: protobuf, generated, code-gen\n---\n\n## Protobuf No Edit\n\n**Impact:** CRITICAL - Never edit files in protogen/ - they are auto-generated\n\nThe `/src/protogen/` directory contains auto-generated TypeScript from protobuf schemas. Any manual edits will be lost on regeneration and may cause type mismatches.\n\n**Incorrect:**\n```typescript\n// WRONG: Editing generated file\n// src/protogen/user_pb.ts\nexport interface User {\n  id: string;\n  name: string;\n  customField: string; // WRONG: Manual addition\n}\n```\n\n**Correct:**\n```typescript\n// CORRECT: Extend types outside protogen\n// src/types/user.ts\nimport { User as ProtoUser } from \"protogen/user_pb\";\n\nexport interface ExtendedUser extends ProtoUser {\n  customField: string;\n}\n\n// CORRECT: Transform data in hooks/utils\n// src/hooks/useUser.ts\nconst { data } = useQuery(getUser, { id });\n\nconst extendedUser = useMemo(() => ({\n  ...data,\n  displayName: `${data?.firstName} ${data?.lastName}`,\n}), [data]);\n```\n\n## Regenerating Protos\n\nWhen proto schemas change upstream:\n\n```bash\n# From repository root (not frontend/)\ntask proto:generate\n```\n\n## Finding Generated Files\n\n```\nsrc/protogen/\n├── *_pb.ts           # Message types\n├── *_connect.ts      # Service definitions\n└── *_connectquery.ts # Connect Query hooks\n```\n\n## Common Imports\n\n```typescript\n// Message types\nimport { User, CreateUserRequest } from \"protogen/user_pb\";\n\n// Service methods\nimport { UserService } from \"protogen/user_connect\";\n\n// Query hooks\nimport { getUser, listUsers, createUser } from \"protogen/user-UserService_connectquery\";\n```\n","frontend/.claude/skills/api-patterns/rules/use-connect-query.md":"---\ntitle: Use Connect Query\nimpact: CRITICAL\nimpactDescription: Consistent API patterns enable caching, refetching, and error handling\ntags: connect-query, react-query, api, grpc\n---\n\n# Use Connect Query (CRITICAL)\n\n## Explanation\n\nAlways use Connect Query hooks (`useQuery`, `useMutation`) for API calls. These integrate with React Query for caching, background refetching, and optimistic updates. Never use raw fetch or manual state management for server data.\n\n## Incorrect\n\n```tsx\n// Manual fetch with useState\nfunction ClusterList() {\n  const [clusters, setClusters] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    fetch('/api/clusters')\n      .then(res => res.json())\n      .then(data => {\n        setClusters(data);\n        setLoading(false);\n      });\n  }, []);\n}\n```\n\n```tsx\n// Missing enabled flag\nconst { data } = useQuery(\n  getClusters,\n  { organizationId: undefined } // Will error!\n);\n```\n\n## Correct\n\n```tsx\nimport { useQuery } from '@connectrpc/connect-query';\nimport { getClusters } from 'proto/frontend/controlplane/v1/cluster-ClusterService_connectquery';\n\nfunction ClusterList({ organizationId }: Props) {\n  const {\n    data: clusters,\n    isLoading,\n    error,\n    refetch,\n  } = useQuery(\n    getClusters,\n    { organizationId },\n    {\n      staleTime: 1000 * 60 * 5, // 5 minutes\n      enabled: !!organizationId, // Only fetch when ID exists\n    }\n  );\n\n  if (isLoading) return <Skeleton />;\n  if (error) return <ErrorDisplay error={error} />;\n\n  return <List items={clusters} />;\n}\n```\n\n## Common Options\n\n| Option | Purpose |\n|--------|---------|\n| `enabled` | Conditional fetching |\n| `staleTime` | Cache duration before refetch |\n| `refetchInterval` | Polling interval |\n| `refetchOnWindowFocus` | Refetch when tab focused |\n\n## Reference\n\n- [Connect Query Docs](https://connectrpc.com/docs/web/query/getting-started)\n- [TanStack Query Options](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery)\n- See also: [use-react-query-for-server](../../state-management/rules/use-react-query-for-server.md) — when to use React Query vs Zustand\n","frontend/.claude/skills/code-standards/SKILL.md":"---\nname: code-standards\ndescription: TypeScript, React, and JavaScript best practices enforced by Ultracite/Biome.\n---\n\n# Code Standards\n\nWrite code that is accessible, performant, type-safe, and maintainable.\n\n## Activation Conditions\n\n- Linting errors need fixing\n- Code formatting issues\n- Code review feedback\n- Questions about style guidelines\n\n## Quick Reference\n\n| Action | Rule |\n|--------|------|\n| Write components | `react-functional-only.md` |\n| Async code | `async-await-promises.md` |\n| Avoid legacy libs | `no-legacy.md` |\n\n`any` types and top-level regex are enforced by Biome (`noExplicitAny`, `useTopLevelRegex`).\n\n## Commands\n\n```bash\nbun x ultracite fix     # Format and fix\nbun x ultracite check   # Check for issues\nbun x ultracite doctor  # Diagnose setup\n```\n\n## Quick Fix\n\nMost issues are auto-fixed:\n\n```bash\nbun x ultracite fix\n```\n\n## Console Logging\n\n- **Never** use `console.log`, `console.debug`, or `console.info` in production code\n- Only use `console.error` and `console.warn` for actionable errors\n- For debug logging, wrap in `if (IsDev) { ... }`\n- Don't use `biome-ignore` to suppress console warnings - remove the logs instead\n\n## Rules\n\nSee `rules/` directory for detailed guidance.\n","frontend/.claude/skills/code-standards/rules/async-await-promises.md":"---\ntitle: Async/Await Over Promise Chains\nimpact: HIGH\nimpactDescription: async/await is more readable and makes error handling explicit\ntags: async, await, promises, javascript\n---\n\n# Async/Await Over Promise Chains (HIGH)\n\n## Explanation\n\nUse `async/await` instead of `.then()` chains. Async/await is more readable, makes error handling explicit, and avoids callback hell. Never leave promises floating without handling.\n\n## Incorrect\n\n```typescript\n// Promise chains\nfunction loadData() {\n  fetchUser()\n    .then(user => fetchPosts(user.id))\n    .then(posts => processPosts(posts))\n    .catch(error => handleError(error));\n}\n\n// Floating promise (no await)\nfunction save() {\n  saveToDatabase(data); // Promise returned but not awaited!\n}\n\n// Async in Promise executor\nnew Promise(async (resolve) => {\n  const data = await fetch(url);\n  resolve(data);\n});\n\n// Mixing patterns\nasync function mixed() {\n  return fetch(url).then(res => res.json());\n}\n```\n\n## Correct\n\n```typescript\n// async/await\nasync function loadData() {\n  try {\n    const user = await fetchUser();\n    const posts = await fetchPosts(user.id);\n    return processPosts(posts);\n  } catch (error) {\n    handleError(error);\n  }\n}\n\n// Always await or return promises\nasync function save() {\n  await saveToDatabase(data);\n}\n\n// Parallel execution when needed\nasync function loadAll() {\n  const [users, posts] = await Promise.all([\n    fetchUsers(),\n    fetchPosts(),\n  ]);\n  return { users, posts };\n}\n\n// Consistent async/await\nasync function fetchJson(url: string) {\n  const response = await fetch(url);\n  return response.json();\n}\n```\n\n## Error Handling\n\n```typescript\n// Explicit try/catch\nasync function riskyOperation() {\n  try {\n    const result = await mightFail();\n    return result;\n  } catch (error) {\n    // Log and rethrow or handle\n    console.error('Operation failed:', error);\n    throw error;\n  }\n}\n\n// Error boundaries for React\n<ErrorBoundary fallback={<ErrorFallback />}>\n  <AsyncComponent />\n</ErrorBoundary>\n```\n\n## Reference\n\n- [MDN async/await](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Promises)\n","frontend/.claude/skills/code-standards/rules/no-legacy.md":"---\ntitle: No Legacy Patterns\nimpact: CRITICAL\nimpactDescription: Never use legacy patterns - they block migration efforts\ntags: legacy, migration, mobx, yup\n---\n\n## No Legacy Patterns\n\n**Impact:** CRITICAL - Never use legacy patterns - they block migration efforts\n\nThe codebase is actively migrating away from legacy libraries. Using them in new code blocks migration and creates tech debt.\n\n## Prohibited Patterns\n\n| Legacy | Modern Alternative |\n|--------|-------------------|\n| MobX, decorators | Zustand for global state |\n| Class components | Functional components + hooks |\n| Yup validation | Zod |\n| `any` type | `unknown` or specific types |\n| Jest | Rstest |\n\n## Common Violations\n\n**MobX:**\n```typescript\n// WRONG\nimport { makeAutoObservable, observable, action } from \"mobx\";\nimport { observer } from \"mobx-react-lite\";\n\n@observer\nclass Store {\n  @observable users = [];\n}\n```\n\n```typescript\n// CORRECT\nimport { create } from \"zustand\";\n\nconst useStore = create<State>((set) => ({\n  users: [],\n  addUser: (user) => set((s) => ({ users: [...s.users, user] })),\n}));\n```\n\n**Class Components:**\n```tsx\n// WRONG\nclass UserProfile extends React.Component<Props, State> {\n  componentDidMount() { /* ... */ }\n  render() { return <div />; }\n}\n```\n\n```tsx\n// CORRECT\nconst UserProfile = ({ userId }: Props) => {\n  const { data } = useQuery(getUser, { id: userId });\n  return <Card>{data?.name}</Card>;\n};\n```\n\n## Migration Guidance\n\nWhen modifying files with legacy patterns:\n1. **Don't add** new legacy imports\n2. **Consider migrating** the component if changes are significant\n3. **Keep changes minimal** if just fixing bugs\n4. **New features** must use modern patterns\n\n### MobX → React State: Check Side-Effects\n\nWhen converting observables to React state, automatic behaviors must become explicit:\n- MobX observable arrays auto-update UI when mutated\n- React state requires explicit `setState()` calls\n- Clearing, resetting, or initializing state needs manual handling\n\n**Checklist**: List all observable mutations → add equivalent `setState` calls.\n\n## Legacy Locations\n\n- MobX stores: `src/state/` (do not add new files)\n\n## See Also\n\n- [use-ui-registry](../../ui-development/rules/use-ui-registry.md) — Registry component usage and installation\n","frontend/.claude/skills/code-standards/rules/react-functional-only.md":"---\ntitle: Functional Components Only\nimpact: HIGH\nimpactDescription: Class components are deprecated patterns that make code harder to test and compose\ntags: react, components, functional, hooks\n---\n\n# Functional Components Only (HIGH)\n\n## Explanation\n\nAlways use functional components with hooks. Class components are a legacy pattern that makes code harder to test, compose, and optimize. Functional components are the modern React standard.\n\n## Incorrect\n\n```tsx\n// Class component\nclass UserProfile extends React.Component<Props, State> {\n  state = { loading: true };\n\n  componentDidMount() {\n    this.fetchUser();\n  }\n\n  render() {\n    return <div>{this.state.loading ? 'Loading...' : this.props.name}</div>;\n  }\n}\n\n// Nested component definition (causes remounting)\nfunction Parent() {\n  function Child() {\n    return <div>Child</div>;\n  }\n  return <Child />; // Child remounts every render!\n}\n\n// Conditional hooks\nfunction Component({ enabled }) {\n  if (enabled) {\n    const [state, setState] = useState(); // Rules of Hooks violation\n  }\n}\n```\n\n## Correct\n\n```tsx\n// Functional component\nexport function UserProfile({ userId }: Props) {\n  const [loading, setLoading] = useState(true);\n  const [user, setUser] = useState<User | null>(null);\n\n  useEffect(() => {\n    fetchUser(userId).then(data => {\n      setUser(data);\n      setLoading(false);\n    });\n  }, [userId]);\n\n  if (loading) return <Skeleton />;\n\n  return <div>{user?.name}</div>;\n}\n\n// Sibling components defined outside\nfunction Parent() {\n  return <Child />;\n}\n\nfunction Child() {\n  return <div>Child</div>;\n}\n\n// Hooks always at top level\nfunction Component({ enabled }: Props) {\n  const [state, setState] = useState<string | null>(null);\n\n  if (!enabled) return null;\n\n  return <div>{state}</div>;\n}\n```\n\n## Component Patterns\n\n```tsx\n// Named export (preferred)\nexport function FeatureComponent() { }\n\n// Colocated sub-components\nexport function Feature() {\n  return (\n    <div>\n      <FeatureHeader />\n      <FeatureContent />\n    </div>\n  );\n}\n\n// Keep sub-components in same file if small\nfunction FeatureHeader() { return <h1>Title</h1>; }\nfunction FeatureContent() { return <p>Content</p>; }\n```\n\n## Reference\n\n- [React Hooks](https://react.dev/reference/react/hooks)\n- [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks)\n","frontend/.claude/skills/e2e-tester/SKILL.md":"---\nname: e2e-tester\ndescription: \"Write and run Rstest/Playwright E2E tests for Redpanda Console using testcontainers. Analyzes test failures, adds missing testids, and improves test stability. Use when user requests E2E tests, Playwright tests, integration tests, test failures, missing testids, or mentions 'test workflow', 'browser testing', 'end-to-end', or 'testcontainers'.\"\nallowed-tools: Read, Write, Edit, Bash, Glob, Grep, Task, mcp__ide__getDiagnostics\n---\n\n# E2E testing with Rstest, Playwright and Testcontainers\n\nWrite end-to-end tests using Playwright against a full Redpanda Console stack running in Docker containers via testcontainers.\n\n## When to Use This Skill\n\n- Testing 2+ step user journeys (login -> action -> verify)\n- Multi-page workflows\n- Browser automation with Playwright\n\n**NOT for:** Component unit tests -> use [testing](../testing/SKILL.md)\n\n## Critical Rules\n\n**ALWAYS:**\n- Run `bun run build` before running E2E tests (frontend assets required)\n- Use `testcontainers` API for container management (never manual `docker` commands in tests)\n- Use `page.getByRole()` and `page.getByLabel()` selectors (avoid CSS selectors)\n- Add `data-testid` attributes when semantic selectors aren't available\n- Run Rstest CLI commands and inspect `.rstest/` artifacts to analyze failures\n- Clean up test data using `afterEach` to call cleanup API endpoints\n\n**NEVER:**\n- Test UI component rendering (use unit/integration tests)\n- Use brittle CSS selectors like `.class-name` or `#id`\n- Use `force:true` on `.click()` or `waitForTimeout`\n- Hard-code wait times (use `waitFor` with conditions)\n- Leave containers running after test failures\n- Commit test screenshots to git\n\n## Commands\n\n```bash\nbun run build                # Build frontend (REQUIRED first!)\nbun run e2e-test:variant console  # OSS E2E\nbun run e2e-test:variant console-enterprise  # Requires ENTERPRISE_BACKEND_DIR\nbun run e2e-test:variant console --watch  # Rstest watcher\nbun run e2e-test:variant console topics/topic-creation.spec.ts\nbun run e2e-test  # All four variants, including enterprise\n```\n\n## Test Architecture\n\n**OSS Mode (`bun run e2e-test:variant console`):** Redpanda + Backend + OwlShop containers\n**Enterprise Mode (`bun run e2e-test:variant console-enterprise`):** Same + RBAC, SSO (requires `console-enterprise` repo)\n\nFile location: `tests/test-variant-<variant>/<feature>/*.spec.ts`.\nImport `test`/`expect` from `tests/shared/e2e.ts` or the variant fixtures.\nSee [runner contracts](../../../tests/README.md).\n\n## Selector Priority\n\n1. `getByRole()` - Best for accessibility\n2. `getByLabel()` - For form inputs\n3. `getByText()` - For content verification\n4. `getByTestId()` - When semantic selectors aren't clear\n5. CSS selectors - Avoid if possible\n\n## Test ID Naming\n\n- kebab-case: `data-testid=\"feature-action-element\"`\n- Specific: include feature name + action + element type\n- Dynamic: `data-testid={\\`item-delete-\\${id}\\`}`\n\n## References\n\n- [Container Setup](references/container-setup.md) — Testcontainer lifecycle, configs, CI setup\n- [Test Patterns](references/test-patterns.md) — Multi-step workflows, forms, tables, API testing\n- [Failure Analysis](references/failure-analysis.md) — Error patterns and CLI debugging\n\n## Output\n\nAfter completing work:\n1. Confirm frontend build succeeded\n2. Verify all E2E tests pass\n3. Note any new test IDs added to components\n4. Mention cleanup of test containers\n","frontend/.claude/skills/e2e-tester/references/container-setup.md":"# Container setup reference\n\nThe maintained lifecycle lives in `tests/shared/global-setup.mjs`, shared by all four\nRstest variant configs. Read [Testcontainers details](../../../../tests/TESTCONTAINERS.md)\nand [test commands](../../../../tests/README.md) before changing it.\n\nBuild the frontend first. Setup copies its assets into the backend build context,\nbuilds an image, starts the configured brokers/services and backend, and waits for\nreadiness. The returned teardown owns live handles; `global-teardown.mjs` provides\nmanual crash recovery using the serialized state. Preserve failure cleanup.\n\nVariant metadata/configuration lives in `tests/test-variant-*/config/`. Images are\npinned in `test-images.json` at the repository root. Local single-variant runs remap host ports;\ncontainer-to-container addresses stay unchanged.\n\nEnterprise requires a separate backend checkout (`ENTERPRISE_BACKEND_DIR`). Global\nsetup authenticates through the real login form and stores browser state under the\ngitignored `playwright/.auth/` directory. Login-error specs override storage with an\nempty state. There is no HTTP Basic-auth replacement for application login.\n\nCI runs three OSS variants in `.github/workflows/frontend-verify.yml`. It installs\nChromium and Linux dependencies, downloads the same-run frontend build, and invokes\n`bun run e2e-test:variant <variant>`. Failure evidence is uploaded from `tests/.rstest/`.\nThe enterprise backend remains the responsibility of downstream enterprise CI.\n\nFor port conflicts, inspect the owner or use the dynamic-port wrapper. Never kill an\nunidentified developer process. For service failures, inspect Docker logs and the\nvariant state file rather than weakening waits or assertions.\n","frontend/.claude/skills/e2e-tester/references/failure-analysis.md":"# Failure Analysis Reference\n\n## CLI and artifacts\n\nRun `bun run e2e-test:variant console list --summary` to collect tests without services.\nRun `bun run e2e-test:variant console topics/topic-list.spec.ts` to reproduce against the\nreal stack. Inspect `.rstest/` screenshots, first-retry videos and Playwright traces.\nEnterprise screenshots/videos remain disabled. Use native locators/DOM inspection for\nselector diagnosis; do not replace real E2E coverage with mocked component tests.\n\n## Common Failure Patterns\n\n### 1. Element Not Found\n\n```\nError: locator.click: Target closed\nError: Timeout 30000ms exceeded waiting for locator\n```\n\n**Analysis steps:**\n1. Check if element has correct testid/role\n2. Verify element is visible (not hidden/collapsed)\n3. Check for timing issues (element loads async)\n4. Look for dynamic content that changes selector\n\n**Fix:**\n```typescript\n// BAD: Element might not be loaded\nawait page.getByRole('button', { name: 'Create' }).click();\n\n// GOOD: Wait for element to be visible\nawait expect(page.getByRole('button', { name: 'Create' })).toBeVisible();\nawait page.getByRole('button', { name: 'Create' }).click();\n\n// BETTER: Add testid for stability\nawait page.getByTestId('create-button').click();\n```\n\n### 2. Selector Ambiguity\n\n```\nError: strict mode violation: locator('button') resolved to 3 elements\n```\n\n**Fix:**\n```typescript\n// BAD: Multiple \"Edit\" buttons on page\nawait page.getByRole('button', { name: 'Edit' }).click();\n\n// GOOD: More specific with testid\nawait page.getByTestId('schema-edit-compatibility-btn').click();\n\n// GOOD: Scope within container\nawait page.getByRole('region', { name: 'Schema Details' })\n          .getByRole('button', { name: 'Edit' }).click();\n```\n\n### 3. Timing/Race Conditions\n\n```\nError: expect(locator).toHaveText()\nExpected string: \"Success\"\nReceived string: \"Loading...\"\n```\n\n**Fix:**\n```typescript\n// BAD: Doesn't wait for state change\nawait page.getByRole('button', { name: 'Save' }).click();\nexpect(page.getByText('Success')).toBeVisible();\n\n// GOOD: Wait for the expected state\nawait page.getByRole('button', { name: 'Save' }).click();\nawait expect(page.getByText('Success')).toBeVisible({ timeout: 5000 });\n```\n\n### 4. Navigation Issues\n\n```\nError: page.goto: net::ERR_CONNECTION_REFUSED\n```\n\n**Fix:**\n```bash\n# Check containers are running\ndocker ps | grep console-backend\n\n# Check container logs\ndocker logs <container-id>\n\n# Verify port mapping\ncurl http://localhost:3000\n\n# Check testcontainer state file\ncat .testcontainers-state.json\n```\n\n## Systematic Failure Analysis Workflow\n\n1. **Get Test Results** — Run the relevant variant CLI and inspect console/artifact output\n2. **Analyze Error Patterns:**\n   - Selector not found -> Missing/wrong testid or element not visible\n   - Strict mode violation -> Need more specific selector\n   - Timeout -> Element loads async, need waitFor\n   - Connection refused -> Container/service not running\n3. **Find Missing Test IDs** — Inspect semantic roles and labels in the source and browser\n4. **Add Test IDs** — Read component, add `data-testid`, follow naming convention\n5. **Update Tests** — Replace brittle selectors with stable testids, add wait conditions\n6. **Verify Fixes** — Run specific test file, then full suite\n\n## Debugging Commands\n\n```bash\n# Check container logs\ndocker ps -a | grep console-backend\ndocker logs <container-id>\n\n# Check if services are accessible\ncurl http://localhost:3000\ncurl http://localhost:19092\n\n# Run with debug output\nDEBUG=pw:api bun run e2e-test\n\n# Keep containers running on failure\nTESTCONTAINERS_RYUK_DISABLED=true bun run e2e-test\n```\n\n**In-test debugging:**\n```typescript\nawait page.pause();  // Opens Playwright Inspector\nawait page.screenshot({ path: 'debug.png' });\nconsole.log(await page.content());\n```\n\n## Test Timeout Issues\n\n```typescript\n// Increase timeout for slow operations\ntest('slow operation', async ({ page }) => {\n  test.setTimeout(60000); // 60 seconds\n  await page.goto('/slow-page');\n  await expect(page.getByText('Loaded')).toBeVisible({ timeout: 30000 });\n});\n```\n","frontend/.claude/skills/e2e-tester/references/test-patterns.md":"# Test Patterns Reference\n\n## Basic Test Structure\n\n```typescript\nimport { test, expect } from '../shared/e2e';\n\ntest.describe('Feature Name', () => {\n  test('user can complete workflow', async ({ page }) => {\n    await page.goto('/feature');\n    await page.getByRole('button', { name: 'Create' }).click();\n    await page.getByLabel('Name').fill('test-item');\n    await page.getByRole('button', { name: 'Submit' }).click();\n    await expect(page.getByText('Success')).toBeVisible();\n    await expect(page).toHaveURL(/\\/feature\\/test-item/);\n  });\n});\n```\n\n## Multi-Step Workflows\n\n```typescript\ntest('user creates, configures, and tests topic', async ({ page }) => {\n  // Step 1: Navigate and create\n  await page.goto('/topics');\n  await page.getByRole('button', { name: 'Create Topic' }).click();\n\n  // Step 2: Fill form\n  await page.getByLabel('Topic Name').fill('test-topic');\n  await page.getByLabel('Partitions').fill('3');\n  await page.getByRole('button', { name: 'Create' }).click();\n\n  // Step 3: Verify creation\n  await expect(page.getByText('Topic created successfully')).toBeVisible();\n  await expect(page).toHaveURL(/\\/topics\\/test-topic/);\n\n  // Step 4: Configure topic\n  await page.getByRole('button', { name: 'Configure' }).click();\n  await page.getByLabel('Retention Hours').fill('24');\n  await page.getByRole('button', { name: 'Save' }).click();\n\n  // Step 5: Verify configuration\n  await expect(page.getByText('Configuration saved')).toBeVisible();\n});\n```\n\n## Testing Forms\n\n```typescript\ntest('form validation works correctly', async ({ page }) => {\n  await page.goto('/create-topic');\n\n  // Submit empty form - should show errors\n  await page.getByRole('button', { name: 'Create' }).click();\n  await expect(page.getByText('Name is required')).toBeVisible();\n\n  // Fill valid data - should succeed\n  await page.getByLabel('Topic Name').fill('valid-topic');\n  await page.getByRole('button', { name: 'Create' }).click();\n  await expect(page.getByText('Success')).toBeVisible();\n});\n```\n\n## Testing Data Tables\n\n```typescript\ntest('user can filter and sort topics', async ({ page }) => {\n  await page.goto('/topics');\n\n  // Filter\n  await page.getByPlaceholder('Search topics').fill('test-');\n  await expect(page.getByRole('row')).toHaveCount(3); // Header + 2 results\n\n  // Sort\n  await page.getByRole('columnheader', { name: 'Name' }).click();\n  const firstRow = page.getByRole('row').nth(1);\n  await expect(firstRow).toContainText('test-topic-a');\n});\n```\n\n## API Interactions\n\n```typescript\ntest('creating topic triggers backend API', async ({ page }) => {\n  // Listen for API call\n  const apiPromise = page.waitForResponse(\n    resp => resp.url().includes('/api/topics') && resp.status() === 201\n  );\n\n  // Trigger action\n  await page.goto('/topics');\n  await page.getByRole('button', { name: 'Create Topic' }).click();\n  await page.getByLabel('Name').fill('api-test-topic');\n  await page.getByRole('button', { name: 'Create' }).click();\n\n  // Verify API was called\n  const response = await apiPromise;\n  const body = await response.json();\n  expect(body.name).toBe('api-test-topic');\n});\n```\n\n## Async Operations\n\n```typescript\n// Wait for specific condition\nawait expect(page.getByRole('status')).toHaveText('Ready');\n\n// Wait for navigation\nawait page.waitForURL('**/topics/my-topic');\n\n// Wait for API response\nawait page.waitForResponse(resp =>\n  resp.url().includes('/api/topics') && resp.status() === 200\n);\n\n// NEVER use fixed timeouts\n// await page.waitForTimeout(5000); // BAD\n```\n\n## Selectors Best Practices\n\n```typescript\n// GOOD: Role-based (accessible)\npage.getByRole('button', { name: 'Create Topic' })\npage.getByLabel('Topic Name')\npage.getByText('Success message')\n\n// GOOD: Test IDs when role isn't clear\npage.getByTestId('topic-list-item')\n\n// BAD: CSS selectors (brittle)\npage.locator('.btn-primary')\npage.locator('#topic-name-input')\n```\n\n## Adding Test IDs to Components\n\n```tsx\n// In React component\n<Button data-testid=\"create-topic-button\">Create Topic</Button>\n\n// In test\nawait page.getByTestId('create-topic-button').click();\n```\n\n**Where to Add:**\n1. Primary actions: Create, Save, Delete, Edit, Submit, Cancel buttons\n2. Navigation: Links to detail pages, breadcrumbs\n3. Forms: All input fields, selects, checkboxes, radio buttons\n4. Lists/Tables: Row identifiers, action buttons within rows\n5. Dialogs/Modals: Open/close buttons, form elements inside\n6. Search/Filter: Search inputs, filter dropdowns, clear buttons\n","frontend/.claude/skills/form-refactorer/SKILL.md":"---\nname: form-refactorer\ndescription: Refactor legacy forms to use modern Redpanda UI Registry Field components with react-hook-form and Zod validation. Use when user requests: (1) Form refactoring or modernization, (2) Updating forms to use Field components, (3) Migrating from legacy form patterns, (4) Implementing forms with react-hook-form and Zod validation.\n---\n\n# Form Refactorer\n\nRefactor legacy forms to modern Redpanda UI Registry Field components with react-hook-form and Zod validation.\n\n## Core Requirements\n\n### MUST USE (Modern Pattern)\n\n- Field component family from `components/redpanda-ui/components/field`\n- react-hook-form for form state management\n- Zod for validation schemas via `zodResolver`\n\n### FORBIDDEN (Legacy Patterns)\n\nSee [no-legacy](../code-standards/rules/no-legacy.md) for prohibited patterns. Also never import `Form` from `components/redpanda-ui/components/form`.\n\n## Component Hierarchy\n\n```\nFieldSet              - Semantic fieldset container for related fields\n├─ FieldLegend        - Title for the fieldset\n├─ FieldDescription   - Description for entire fieldset\n└─ FieldGroup         - Layout wrapper that stacks fields\n   ├─ Field           - Individual field wrapper\n   │  ├─ FieldLabel       - Label for input (use htmlFor)\n   │  ├─ Input/Select/... - Form control\n   │  ├─ FieldDescription - Helper text\n   │  └─ FieldError       - Error message\n   ├─ FieldSeparator  - Visual divider between fields\n   └─ Field           - Another field\n```\n\n## Key Patterns\n\n**Vertical field:**\n```tsx\n<Field data-invalid={!!errors.fieldName}>\n  <FieldLabel htmlFor=\"fieldName\">Label</FieldLabel>\n  <Input id=\"fieldName\" {...register(\"fieldName\")} aria-invalid={!!errors.fieldName} />\n  {errors.fieldName && <FieldError>{errors.fieldName.message}</FieldError>}\n</Field>\n```\n\n**Horizontal field (switch/checkbox):**\n```tsx\n<Field orientation=\"horizontal\">\n  <Switch id=\"toggle\" {...register(\"toggle\")} />\n  <FieldContent>\n    <FieldLabel htmlFor=\"toggle\">Enable Feature</FieldLabel>\n    <FieldDescription>Description</FieldDescription>\n  </FieldContent>\n</Field>\n```\n\n## Checklist\n\nBefore completing refactoring, verify:\n\n- [ ] All legacy form imports removed (the legacy `Form` component)\n- [ ] Field components used for all fields\n- [ ] FieldLabel has `htmlFor` matching Input `id`\n- [ ] Errors show `data-invalid` on Field and `aria-invalid` on Input\n- [ ] Yup replaced with Zod + zodResolver\n- [ ] FieldSet + FieldLegend used for semantic grouping\n- [ ] FieldError conditionally rendered: `{errors.field && <FieldError>...`\n- [ ] Form submission handled via react-hook-form `handleSubmit`\n\n## References\n\n- [Common Patterns](references/common-patterns.md) — Dynamic fields, field arrays, multi-section forms, pitfalls, Zod patterns\n- [Field Component Docs](https://redpanda-ui-registry.netlify.app/docs/field)\n","frontend/.claude/skills/form-refactorer/references/common-patterns.md":"# Common Form Patterns\n\nThis document provides patterns for common form scenarios using Field components.\n\n## Dynamic Fields\n\nForms with conditionally rendered fields based on user input.\n\n```tsx\nimport { useForm, useWatch } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { z } from \"zod\";\nimport {\n  FieldSet,\n  FieldLegend,\n  FieldGroup,\n  Field,\n  FieldLabel,\n  FieldError\n} from \"components/redpanda-ui/components/field\";\nimport { Select } from \"components/redpanda-ui/components/select\";\nimport { Input } from \"components/redpanda-ui/components/input\";\n\nconst schema = z.object({\n  authType: z.enum([\"none\", \"basic\", \"token\"]),\n  username: z.string().optional(),\n  password: z.string().optional(),\n  token: z.string().optional(),\n}).refine((data) => {\n  if (data.authType === \"basic\") {\n    return !!data.username && !!data.password;\n  }\n  if (data.authType === \"token\") {\n    return !!data.token;\n  }\n  return true;\n}, {\n  message: \"Authentication fields are required\",\n});\n\nexport const AuthForm = () => {\n  const { register, control, formState: { errors } } = useForm({\n    resolver: zodResolver(schema),\n  });\n\n  const authType = useWatch({ control, name: \"authType\" });\n\n  return (\n    <FieldSet>\n      <FieldLegend>Authentication</FieldLegend>\n      <FieldGroup>\n        <Field data-invalid={!!errors.authType}>\n          <FieldLabel htmlFor=\"authType\">Authentication Type</FieldLabel>\n          <Select id=\"authType\" {...register(\"authType\")}>\n            <option value=\"none\">None</option>\n            <option value=\"basic\">Basic Auth</option>\n            <option value=\"token\">Token</option>\n          </Select>\n          {errors.authType && <FieldError>{errors.authType.message}</FieldError>}\n        </Field>\n\n        {authType === \"basic\" && (\n          <>\n            <Field data-invalid={!!errors.username}>\n              <FieldLabel htmlFor=\"username\">Username</FieldLabel>\n              <Input id=\"username\" {...register(\"username\")} aria-invalid={!!errors.username} />\n              {errors.username && <FieldError>{errors.username.message}</FieldError>}\n            </Field>\n\n            <Field data-invalid={!!errors.password}>\n              <FieldLabel htmlFor=\"password\">Password</FieldLabel>\n              <Input id=\"password\" type=\"password\" {...register(\"password\")} aria-invalid={!!errors.password} />\n              {errors.password && <FieldError>{errors.password.message}</FieldError>}\n            </Field>\n          </>\n        )}\n\n        {authType === \"token\" && (\n          <Field data-invalid={!!errors.token}>\n            <FieldLabel htmlFor=\"token\">API Token</FieldLabel>\n            <Input id=\"token\" {...register(\"token\")} aria-invalid={!!errors.token} />\n            {errors.token && <FieldError>{errors.token.message}</FieldError>}\n          </Field>\n        )}\n      </FieldGroup>\n    </FieldSet>\n  );\n};\n```\n\n## Field Arrays (Repeatable Fields)\n\nForms with dynamically added/removed field groups.\n\n```tsx\nimport { useForm, useFieldArray } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { z } from \"zod\";\nimport {\n  FieldSet,\n  FieldLegend,\n  FieldGroup,\n  Field,\n  FieldLabel,\n  FieldError,\n  FieldSeparator\n} from \"components/redpanda-ui/components/field\";\nimport { Input } from \"components/redpanda-ui/components/input\";\nimport { Button } from \"components/redpanda-ui/components/button\";\n\nconst schema = z.object({\n  tags: z.array(z.object({\n    key: z.string().min(1, \"Key is required\"),\n    value: z.string().min(1, \"Value is required\"),\n  })).min(1, \"At least one tag is required\"),\n});\n\nexport const TagsForm = () => {\n  const { register, control, formState: { errors } } = useForm({\n    resolver: zodResolver(schema),\n    defaultValues: {\n      tags: [{ key: \"\", value: \"\" }],\n    },\n  });\n\n  const { fields, append, remove } = useFieldArray({\n    control,\n    name: \"tags\",\n  });\n\n  return (\n    <FieldSet>\n      <FieldLegend>Tags</FieldLegend>\n      <FieldGroup>\n        {fields.map((field, index) => (\n          <div key={field.id}>\n            {index > 0 && <FieldSeparator />}\n            <FieldGroup>\n              <Field data-invalid={!!errors.tags?.[index]?.key}>\n                <FieldLabel htmlFor={`tags.${index}.key`}>Key</FieldLabel>\n                <Input\n                  id={`tags.${index}.key`}\n                  {...register(`tags.${index}.key`)}\n                  aria-invalid={!!errors.tags?.[index]?.key}\n                />\n                {errors.tags?.[index]?.key && (\n                  <FieldError>{errors.tags[index].key.message}</FieldError>\n                )}\n              </Field>\n\n              <Field data-invalid={!!errors.tags?.[index]?.value}>\n                <FieldLabel htmlFor={`tags.${index}.value`}>Value</FieldLabel>\n                <Input\n                  id={`tags.${index}.value`}\n                  {...register(`tags.${index}.value`)}\n                  aria-invalid={!!errors.tags?.[index]?.value}\n                />\n                {errors.tags?.[index]?.value && (\n                  <FieldError>{errors.tags[index].value.message}</FieldError>\n                )}\n              </Field>\n\n              {fields.length > 1 && (\n                <Button\n                  type=\"button\"\n                  variant=\"destructive\"\n                  onClick={() => remove(index)}\n                >\n                  Remove Tag\n                </Button>\n              )}\n            </FieldGroup>\n          </div>\n        ))}\n\n        <Button\n          type=\"button\"\n          variant=\"secondary\"\n          onClick={() => append({ key: \"\", value: \"\" })}\n        >\n          Add Tag\n        </Button>\n      </FieldGroup>\n    </FieldSet>\n  );\n};\n```\n\n## Multi-Section Forms\n\nForms organized into multiple logical sections.\n\n```tsx\nimport {\n  FieldSet,\n  FieldLegend,\n  FieldDescription,\n  FieldGroup,\n  Field,\n  FieldLabel,\n  FieldSeparator,\n  FieldError\n} from \"components/redpanda-ui/components/field\";\nimport { Input } from \"components/redpanda-ui/components/input\";\nimport { Textarea } from \"components/redpanda-ui/components/textarea\";\nimport { Switch } from \"components/redpanda-ui/components/switch\";\n\nexport const ProfileSettingsForm = () => {\n  const { register, formState: { errors } } = useForm({\n    resolver: zodResolver(schema),\n  });\n\n  return (\n    <form>\n      {/* Personal Information Section */}\n      <FieldSet>\n        <FieldLegend>Personal Information</FieldLegend>\n        <FieldDescription>\n          This information will be displayed on your public profile.\n        </FieldDescription>\n        <FieldGroup>\n          <Field data-invalid={!!errors.name}>\n            <FieldLabel htmlFor=\"name\">Full Name</FieldLabel>\n            <Input id=\"name\" {...register(\"name\")} aria-invalid={!!errors.name} />\n            {errors.name && <FieldError>{errors.name.message}</FieldError>}\n          </Field>\n\n          <Field data-invalid={!!errors.bio}>\n            <FieldLabel htmlFor=\"bio\">Bio</FieldLabel>\n            <Textarea id=\"bio\" {...register(\"bio\")} aria-invalid={!!errors.bio} />\n            {errors.bio && <FieldError>{errors.bio.message}</FieldError>}\n          </Field>\n        </FieldGroup>\n      </FieldSet>\n\n      <FieldSeparator />\n\n      {/* Notification Preferences Section */}\n      <FieldSet>\n        <FieldLegend>Notification Preferences</FieldLegend>\n        <FieldDescription>\n          Manage how you receive notifications.\n        </FieldDescription>\n        <FieldGroup>\n          <Field orientation=\"horizontal\">\n            <Switch id=\"emailNotifications\" {...register(\"emailNotifications\")} />\n            <FieldContent>\n              <FieldLabel htmlFor=\"emailNotifications\">Email Notifications</FieldLabel>\n              <FieldDescription>Receive notifications via email</FieldDescription>\n            </FieldContent>\n          </Field>\n\n          <Field orientation=\"horizontal\">\n            <Switch id=\"pushNotifications\" {...register(\"pushNotifications\")} />\n            <FieldContent>\n              <FieldLabel htmlFor=\"pushNotifications\">Push Notifications</FieldLabel>\n              <FieldDescription>Receive push notifications in browser</FieldDescription>\n            </FieldContent>\n          </Field>\n        </FieldGroup>\n      </FieldSet>\n    </form>\n  );\n};\n```\n\n## Read-Only Fields\n\nDisplay form fields in a read-only state.\n\n```tsx\nimport {\n  Field,\n  FieldLabel,\n  FieldDescription\n} from \"components/redpanda-ui/components/field\";\nimport { Input } from \"components/redpanda-ui/components/input\";\n\nexport const ServerDetailsForm = ({ server }) => {\n  return (\n    <FieldGroup>\n      <Field>\n        <FieldLabel htmlFor=\"id\">Server ID</FieldLabel>\n        <Input id=\"id\" value={server.id} disabled />\n        <FieldDescription>Auto-generated unique identifier</FieldDescription>\n      </Field>\n\n      <Field>\n        <FieldLabel htmlFor=\"createdAt\">Created At</FieldLabel>\n        <Input id=\"createdAt\" value={server.createdAt} disabled />\n      </Field>\n    </FieldGroup>\n  );\n};\n```\n\n## Inline Form Actions\n\nForms with actions positioned within the field group.\n\n```tsx\nimport {\n  FieldSet,\n  FieldLegend,\n  FieldGroup,\n  Field,\n  FieldLabel,\n  FieldError\n} from \"components/redpanda-ui/components/field\";\nimport { Input } from \"components/redpanda-ui/components/input\";\nimport { Button } from \"components/redpanda-ui/components/button\";\n\nexport const InlineEditForm = () => {\n  const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm();\n\n  return (\n    <form onSubmit={handleSubmit(onSubmit)}>\n      <FieldSet>\n        <FieldLegend>Quick Edit</FieldLegend>\n        <FieldGroup>\n          <Field data-invalid={!!errors.name}>\n            <FieldLabel htmlFor=\"name\">Name</FieldLabel>\n            <div className=\"flex gap-2\">\n              <Input\n                id=\"name\"\n                {...register(\"name\")}\n                aria-invalid={!!errors.name}\n                className=\"flex-1\"\n              />\n              <Button type=\"submit\" disabled={isSubmitting}>\n                Save\n              </Button>\n            </div>\n            {errors.name && <FieldError>{errors.name.message}</FieldError>}\n          </Field>\n        </FieldGroup>\n      </FieldSet>\n    </form>\n  );\n};\n```\n\n## Common Zod Patterns\n\n```tsx\n// String validations\nz.string().min(1, \"Required\")\nz.string().email(\"Invalid email\")\nz.string().url(\"Invalid URL\")\nz.string().regex(/^[A-Z]/, \"Must start with capital\")\n\n// Number validations\nz.number().min(0, \"Must be positive\")\nz.number().max(100, \"Max 100\")\nz.number().int(\"Must be integer\")\n\n// Optional fields\nz.string().optional()\nz.string().nullable()\n\n// Arrays\nz.array(z.string()).min(1, \"At least one required\")\n\n// Objects\nz.object({\n  nested: z.string(),\n})\n\n// Conditional validation\nz.object({\n  type: z.enum([\"a\", \"b\"]),\n  value: z.string(),\n}).refine((data) => {\n  if (data.type === \"a\") return data.value.length > 5;\n  return true;\n}, \"Value must be longer than 5 for type A\")\n```\n\n## Common Pitfalls\n\n### Pitfall 1: Missing htmlFor/id Association\n\n```tsx\n// WRONG:\n<Field>\n  <FieldLabel>Email</FieldLabel>\n  <Input name=\"email\" />\n</Field>\n\n// CORRECT:\n<Field>\n  <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n  <Input id=\"email\" name=\"email\" />\n</Field>\n```\n\n### Pitfall 2: Missing aria-invalid\n\n```tsx\n// WRONG:\n<Field data-invalid={!!errors.email}>\n  <Input {...register(\"email\")} />\n</Field>\n\n// CORRECT:\n<Field data-invalid={!!errors.email}>\n  <Input {...register(\"email\")} aria-invalid={!!errors.email} />\n</Field>\n```\n\n### Pitfall 3: Conditional FieldError Without Check\n\n```tsx\n// WRONG:\n<FieldError>{errors.email.message}</FieldError>\n\n// CORRECT:\n{errors.email && <FieldError>{errors.email.message}</FieldError>}\n```\n\n### Pitfall 4: Using Legacy Form Components\n\n```tsx\n// WRONG:\nimport { Form } from \"components/redpanda-ui/components/form\";\n\n// CORRECT:\nimport { Field, FieldLabel } from \"components/redpanda-ui/components/field\";\n```\n\n### Pitfall 5: Forgetting FieldContent for Horizontal Fields\n\n```tsx\n// WRONG:\n<Field orientation=\"horizontal\">\n  <Switch id=\"notifications\" />\n  <FieldLabel htmlFor=\"notifications\">Enable</FieldLabel>\n  <FieldDescription>Receive updates</FieldDescription>\n</Field>\n\n// CORRECT:\n<Field orientation=\"horizontal\">\n  <Switch id=\"notifications\" />\n  <FieldContent>\n    <FieldLabel htmlFor=\"notifications\">Enable</FieldLabel>\n    <FieldDescription>Receive updates</FieldDescription>\n  </FieldContent>\n</Field>\n```\n","frontend/.claude/skills/react-best-practices/SKILL.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Freact-best-practices%2FSKILL.md -->","frontend/.claude/skills/react-best-practices/rules/advanced-event-handler-refs.md":"---\ntitle: Store Event Handlers in Refs\nimpact: LOW\nimpactDescription: stable subscriptions\ntags: advanced, hooks, refs, event-handlers, optimization\n---\n\n## Store Event Handlers in Refs\n\nStore callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.\n\n**Incorrect (re-subscribes on every render):**\n\n```tsx\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  useEffect(() => {\n    window.addEventListener(event, handler)\n    return () => window.removeEventListener(event, handler)\n  }, [event, handler])\n}\n```\n\n**Correct (stable subscription):**\n\n```tsx\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  const handlerRef = useRef(handler)\n  useEffect(() => {\n    handlerRef.current = handler\n  }, [handler])\n\n  useEffect(() => {\n    const listener = (e) => handlerRef.current(e)\n    window.addEventListener(event, listener)\n    return () => window.removeEventListener(event, listener)\n  }, [event])\n}\n```\n\n**Alternative: use `useEffectEvent` if you're on latest React:**\n\n```tsx\nimport { useEffectEvent } from 'react'\n\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  const onEvent = useEffectEvent(handler)\n\n  useEffect(() => {\n    window.addEventListener(event, onEvent)\n    return () => window.removeEventListener(event, onEvent)\n  }, [event])\n}\n```\n\n`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.\n","frontend/.claude/skills/react-best-practices/rules/advanced-use-latest.md":"---\ntitle: useLatest for Stable Callback Refs\nimpact: LOW\nimpactDescription: prevents effect re-runs\ntags: advanced, hooks, useLatest, refs, optimization\n---\n\n## useLatest for Stable Callback Refs\n\nAccess latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.\n\n**Implementation:**\n\n```typescript\nfunction useLatest<T>(value: T) {\n  const ref = useRef(value)\n  useLayoutEffect(() => {\n    ref.current = value\n  }, [value])\n  return ref\n}\n```\n\n**Incorrect (effect re-runs on every callback change):**\n\n```tsx\nfunction SearchInput({ onSearch }: { onSearch: (q: string) => void }) {\n  const [query, setQuery] = useState('')\n\n  useEffect(() => {\n    const timeout = setTimeout(() => onSearch(query), 300)\n    return () => clearTimeout(timeout)\n  }, [query, onSearch])\n}\n```\n\n**Correct (stable effect, fresh callback):**\n\n```tsx\nfunction SearchInput({ onSearch }: { onSearch: (q: string) => void }) {\n  const [query, setQuery] = useState('')\n  const onSearchRef = useLatest(onSearch)\n\n  useEffect(() => {\n    const timeout = setTimeout(() => onSearchRef.current(query), 300)\n    return () => clearTimeout(timeout)\n  }, [query])\n}\n```\n","frontend/.claude/skills/react-best-practices/rules/async-defer-await.md":"---\ntitle: Defer Await Until Needed\nimpact: HIGH\nimpactDescription: avoids blocking unused code paths\ntags: async, await, conditional, optimization\n---\n\n## Defer Await Until Needed\n\nMove `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.\n\n**Incorrect (blocks both branches):**\n\n```typescript\nasync function handleRequest(userId: string, skipProcessing: boolean) {\n  const userData = await fetchUserData(userId)\n  \n  if (skipProcessing) {\n    // Returns immediately but still waited for userData\n    return { skipped: true }\n  }\n  \n  // Only this branch uses userData\n  return processUserData(userData)\n}\n```\n\n**Correct (only blocks when needed):**\n\n```typescript\nasync function handleRequest(userId: string, skipProcessing: boolean) {\n  if (skipProcessing) {\n    // Returns immediately without waiting\n    return { skipped: true }\n  }\n  \n  // Fetch only when needed\n  const userData = await fetchUserData(userId)\n  return processUserData(userData)\n}\n```\n\n**Another example (early return optimization):**\n\n```typescript\n// Incorrect: always fetches permissions\nasync function updateResource(resourceId: string, userId: string) {\n  const permissions = await fetchPermissions(userId)\n  const resource = await getResource(resourceId)\n  \n  if (!resource) {\n    return { error: 'Not found' }\n  }\n  \n  if (!permissions.canEdit) {\n    return { error: 'Forbidden' }\n  }\n  \n  return await updateResourceData(resource, permissions)\n}\n\n// Correct: fetches only when needed\nasync function updateResource(resourceId: string, userId: string) {\n  const resource = await getResource(resourceId)\n  \n  if (!resource) {\n    return { error: 'Not found' }\n  }\n  \n  const permissions = await fetchPermissions(userId)\n  \n  if (!permissions.canEdit) {\n    return { error: 'Forbidden' }\n  }\n  \n  return await updateResourceData(resource, permissions)\n}\n```\n\nThis optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.\n","frontend/.claude/skills/react-best-practices/rules/async-dependencies.md":"---\ntitle: Dependency-Based Parallelization\nimpact: CRITICAL\nimpactDescription: 2-10× improvement\ntags: async, parallelization, dependencies, better-all\n---\n\n## Dependency-Based Parallelization\n\nFor operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.\n\n**Incorrect (profile waits for config unnecessarily):**\n\n```typescript\nconst [user, config] = await Promise.all([\n  fetchUser(),\n  fetchConfig()\n])\nconst profile = await fetchProfile(user.id)\n```\n\n**Correct (config and profile run in parallel):**\n\n```typescript\nimport { all } from 'better-all'\n\nconst { user, config, profile } = await all({\n  async user() { return fetchUser() },\n  async config() { return fetchConfig() },\n  async profile() {\n    return fetchProfile((await this.$.user).id)\n  }\n})\n```\n\nReference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)\n","frontend/.claude/skills/react-best-practices/rules/async-suspense-boundaries.md":"---\ntitle: Strategic Suspense Boundaries\nimpact: HIGH\nimpactDescription: faster initial paint\ntags: async, suspense, streaming, layout-shift\n---\n\n## Strategic Suspense Boundaries\n\nInstead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.\n\n**Incorrect (wrapper blocked by data fetching):**\n\n```tsx\nasync function Page() {\n  const data = await fetchData() // Blocks entire page\n  \n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <div>\n        <DataDisplay data={data} />\n      </div>\n      <div>Footer</div>\n    </div>\n  )\n}\n```\n\nThe entire layout waits for data even though only the middle section needs it.\n\n**Correct (wrapper shows immediately, data streams in):**\n\n```tsx\nfunction Page() {\n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <div>\n        <Suspense fallback={<Skeleton />}>\n          <DataDisplay />\n        </Suspense>\n      </div>\n      <div>Footer</div>\n    </div>\n  )\n}\n\nasync function DataDisplay() {\n  const data = await fetchData() // Only blocks this component\n  return <div>{data.content}</div>\n}\n```\n\nSidebar, Header, and Footer render immediately. Only DataDisplay waits for data.\n\n**Alternative (share promise across components):**\n\n```tsx\nfunction Page() {\n  // Start fetch immediately, but don't await\n  const dataPromise = fetchData()\n  \n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <Suspense fallback={<Skeleton />}>\n        <DataDisplay dataPromise={dataPromise} />\n        <DataSummary dataPromise={dataPromise} />\n      </Suspense>\n      <div>Footer</div>\n    </div>\n  )\n}\n\nfunction DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {\n  const data = use(dataPromise) // Unwraps the promise\n  return <div>{data.content}</div>\n}\n\nfunction DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {\n  const data = use(dataPromise) // Reuses the same promise\n  return <div>{data.summary}</div>\n}\n```\n\nBoth components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.\n\n**When NOT to use this pattern:**\n\n- Critical data needed for layout decisions (affects positioning)\n- SEO-critical content above the fold\n- Small, fast queries where suspense overhead isn't worth it\n- When you want to avoid layout shift (loading → content jump)\n\n**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.\n","frontend/.claude/skills/react-best-practices/rules/bundle-barrel-imports.md":"---\ntitle: Avoid Barrel File Imports\nimpact: CRITICAL\nimpactDescription: 200-800ms import cost, slow builds\ntags: bundle, imports, tree-shaking, barrel-files, performance\n---\n\n## Avoid Barrel File Imports\n\nImport directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).\n\nPopular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.\n\n**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.\n\n**Incorrect (imports entire library):**\n\n```tsx\nimport { Check, X, Menu } from 'lucide-react'\n// Loads 1,583 modules, takes ~2.8s extra in dev\n// Runtime cost: 200-800ms on every cold start\n\nimport { Button, TextField } from '@mui/material'\n// Loads 2,225 modules, takes ~4.2s extra in dev\n```\n\n**Correct (imports only what you need):**\n\n```tsx\nimport Check from 'lucide-react/dist/esm/icons/check'\nimport X from 'lucide-react/dist/esm/icons/x'\nimport Menu from 'lucide-react/dist/esm/icons/menu'\n// Loads only 3 modules (~2KB vs ~1MB)\n\nimport Button from '@mui/material/Button'\nimport TextField from '@mui/material/TextField'\n// Loads only what you use\n```\n\nDirect imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.\n\nLibraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.\n\nReference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)\n","frontend/.claude/skills/react-best-practices/rules/bundle-code-splitting.md":"---\ntitle: Code Splitting and Lazy Loading\nimpact: CRITICAL\nimpactDescription: directly affects TTI and LCP\ntags: bundle, dynamic-import, code-splitting, react-lazy, conditional-loading\n---\n\n## Code Splitting and Lazy Loading\n\nUse `React.lazy` with `Suspense` to lazy-load large components not needed on initial render. Load large data modules conditionally. Defer non-critical third-party libraries.\n\n### Dynamic Imports for Heavy Components\n\n**Incorrect (Monaco bundles with main chunk ~300KB):**\n\n```tsx\nimport { MonacoEditor } from './monaco-editor'\n```\n\n**Correct (Monaco loads on demand):**\n\n```tsx\nimport { lazy, Suspense } from 'react'\n\nconst MonacoEditor = lazy(() =>\n  import('./monaco-editor').then(m => ({ default: m.MonacoEditor }))\n)\n\nfunction CodePanel({ code }: { code: string }) {\n  return (\n    <Suspense fallback={<div>Loading editor...</div>}>\n      <MonacoEditor value={code} />\n    </Suspense>\n  )\n}\n```\n\n### Conditional Module Loading\n\nLoad large data or modules only when a feature is activated:\n\n```tsx\nfunction AnimationPlayer({ enabled, setEnabled }: Props) {\n  const [frames, setFrames] = useState<Frame[] | null>(null)\n\n  useEffect(() => {\n    if (enabled && !frames) {\n      import('./animation-frames.js')\n        .then(mod => setFrames(mod.frames))\n        .catch(() => setEnabled(false))\n    }\n  }, [enabled, frames, setEnabled])\n\n  if (!frames) return <Skeleton />\n  return <Canvas frames={frames} />\n}\n```\n\n### Defer Non-Critical Third-Party Libraries\n\nAnalytics, logging, and error tracking don't block user interaction:\n\n```tsx\nconst Analytics = lazy(() =>\n  import('@some-vendor/analytics').then(m => ({ default: m.Analytics }))\n)\n\nexport function App({ children }) {\n  return (\n    <div>\n      {children}\n      <Suspense fallback={null}>\n        <Analytics />\n      </Suspense>\n    </div>\n  )\n}\n```\n","frontend/.claude/skills/react-best-practices/rules/bundle-preload.md":"---\ntitle: Preload Based on User Intent\nimpact: MEDIUM\nimpactDescription: reduces perceived latency\ntags: bundle, preload, user-intent, hover\n---\n\n## Preload Based on User Intent\n\nPreload heavy bundles before they're needed to reduce perceived latency.\n\n**Example (preload on hover/focus):**\n\n```tsx\nfunction EditorButton({ onClick }: { onClick: () => void }) {\n  const preload = () => {\n    if (typeof window !== 'undefined') {\n      void import('./monaco-editor')\n    }\n  }\n\n  return (\n    <button\n      onMouseEnter={preload}\n      onFocus={preload}\n      onClick={onClick}\n    >\n      Open Editor\n    </button>\n  )\n}\n```\n\n**Example (preload when feature flag is enabled):**\n\n```tsx\nfunction FlagsProvider({ children, flags }: Props) {\n  useEffect(() => {\n    if (flags.editorEnabled && typeof window !== 'undefined') {\n      void import('./monaco-editor').then(mod => mod.init())\n    }\n  }, [flags.editorEnabled])\n\n  return <FlagsContext.Provider value={flags}>\n    {children}\n  </FlagsContext.Provider>\n}\n```\n\nThe `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.\n","frontend/.claude/skills/react-best-practices/rules/client-event-listeners.md":"---\ntitle: Deduplicate Global Event Listeners\nimpact: LOW\nimpactDescription: single listener for N components\ntags: client, swr, event-listeners, subscription\n---\n\n## Deduplicate Global Event Listeners\n\nUse `useSWRSubscription()` to share global event listeners across component instances.\n\n**Incorrect (N instances = N listeners):**\n\n```tsx\nfunction useKeyboardShortcut(key: string, callback: () => void) {\n  useEffect(() => {\n    const handler = (e: KeyboardEvent) => {\n      if (e.metaKey && e.key === key) {\n        callback()\n      }\n    }\n    window.addEventListener('keydown', handler)\n    return () => window.removeEventListener('keydown', handler)\n  }, [key, callback])\n}\n```\n\nWhen using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.\n\n**Correct (N instances = 1 listener):**\n\n```tsx\nimport useSWRSubscription from 'swr/subscription'\n\n// Module-level Map to track callbacks per key\nconst keyCallbacks = new Map<string, Set<() => void>>()\n\nfunction useKeyboardShortcut(key: string, callback: () => void) {\n  // Register this callback in the Map\n  useEffect(() => {\n    if (!keyCallbacks.has(key)) {\n      keyCallbacks.set(key, new Set())\n    }\n    keyCallbacks.get(key)!.add(callback)\n\n    return () => {\n      const set = keyCallbacks.get(key)\n      if (set) {\n        set.delete(callback)\n        if (set.size === 0) {\n          keyCallbacks.delete(key)\n        }\n      }\n    }\n  }, [key, callback])\n\n  useSWRSubscription('global-keydown', () => {\n    const handler = (e: KeyboardEvent) => {\n      if (e.metaKey && keyCallbacks.has(e.key)) {\n        keyCallbacks.get(e.key)!.forEach(cb => cb())\n      }\n    }\n    window.addEventListener('keydown', handler)\n    return () => window.removeEventListener('keydown', handler)\n  })\n}\n\nfunction Profile() {\n  // Multiple shortcuts will share the same listener\n  useKeyboardShortcut('p', () => { /* ... */ }) \n  useKeyboardShortcut('k', () => { /* ... */ })\n  // ...\n}\n```\n","frontend/.claude/skills/react-best-practices/rules/client-passive-event-listeners.md":"---\ntitle: Use Passive Event Listeners for Scrolling Performance\nimpact: MEDIUM\nimpactDescription: eliminates scroll delay caused by event listeners\ntags: client, event-listeners, scrolling, performance, touch, wheel\n---\n\n## Use Passive Event Listeners for Scrolling Performance\n\nAdd `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay.\n\n**Incorrect:**\n\n```typescript\nuseEffect(() => {\n  const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)\n  const handleWheel = (e: WheelEvent) => console.log(e.deltaY)\n  \n  document.addEventListener('touchstart', handleTouch)\n  document.addEventListener('wheel', handleWheel)\n  \n  return () => {\n    document.removeEventListener('touchstart', handleTouch)\n    document.removeEventListener('wheel', handleWheel)\n  }\n}, [])\n```\n\n**Correct:**\n\n```typescript\nuseEffect(() => {\n  const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)\n  const handleWheel = (e: WheelEvent) => console.log(e.deltaY)\n  \n  document.addEventListener('touchstart', handleTouch, { passive: true })\n  document.addEventListener('wheel', handleWheel, { passive: true })\n  \n  return () => {\n    document.removeEventListener('touchstart', handleTouch)\n    document.removeEventListener('wheel', handleWheel)\n  }\n}, [])\n```\n\n**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.\n\n**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.\n","frontend/.claude/skills/react-best-practices/rules/js-batch-dom-css.md":"---\ntitle: Batch DOM CSS Changes\nimpact: MEDIUM\nimpactDescription: reduces reflows/repaints\ntags: javascript, dom, css, performance, reflow\n---\n\n## Batch DOM CSS Changes\n\nAvoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.\n\n**Incorrect (interleaved reads and writes force reflows):**\n\n```typescript\nfunction updateElementStyles(element: HTMLElement) {\n  element.style.width = '100px'\n  const width = element.offsetWidth  // Forces reflow\n  element.style.height = '200px'\n  const height = element.offsetHeight  // Forces another reflow\n}\n```\n\n**Correct (batch writes, then read once):**\n\n```typescript\nfunction updateElementStyles(element: HTMLElement) {\n  // Batch all writes together\n  element.style.width = '100px'\n  element.style.height = '200px'\n  element.style.backgroundColor = 'blue'\n  element.style.border = '1px solid black'\n  \n  // Read after all writes are done (single reflow)\n  const { width, height } = element.getBoundingClientRect()\n}\n```\n\n**Better: use CSS classes**\n\n```css\n.highlighted-box {\n  width: 100px;\n  height: 200px;\n  background-color: blue;\n  border: 1px solid black;\n}\n```\n\n```typescript\nfunction updateElementStyles(element: HTMLElement) {\n  element.classList.add('highlighted-box')\n\n  const { width, height } = element.getBoundingClientRect()\n}\n```\n\nPrefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.","frontend/.claude/skills/react-best-practices/rules/js-caching-patterns.md":"---\ntitle: Client-Side Caching Patterns\nimpact: MEDIUM\nimpactDescription: reduces redundant computation and expensive I/O\ntags: javascript, cache, memoization, localStorage, storage, performance\n---\n\n## Client-Side Caching Patterns\n\n### Cache Repeated Function Calls\n\nUse a module-level Map to cache function results when the same function is called repeatedly with the same inputs.\n\n```typescript\nconst slugifyCache = new Map<string, string>()\n\nfunction cachedSlugify(text: string): string {\n  if (slugifyCache.has(text)) return slugifyCache.get(text)!\n  const result = slugify(text)\n  slugifyCache.set(text, result)\n  return result\n}\n```\n\nUse a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.\n\n### Cache Storage API Calls\n\n`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory:\n\n```typescript\nconst storageCache = new Map<string, string | null>()\n\nfunction getLocalStorage(key: string) {\n  if (!storageCache.has(key)) {\n    storageCache.set(key, localStorage.getItem(key))\n  }\n  return storageCache.get(key)\n}\n\nfunction setLocalStorage(key: string, value: string) {\n  localStorage.setItem(key, value)\n  storageCache.set(key, value)\n}\n```\n\nInvalidate on external changes:\n\n```typescript\nwindow.addEventListener('storage', (e) => {\n  if (e.key) storageCache.delete(e.key)\n})\n```\n\n### Version localStorage Data\n\nAdd version prefix to keys and store only needed fields:\n\n```typescript\nconst VERSION = 'v2'\n\nfunction saveConfig(config: { theme: string; language: string }) {\n  try {\n    localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))\n  } catch {\n    // Throws in incognito, quota exceeded, or disabled\n  }\n}\n```\n\nAlways wrap `getItem()`/`setItem()` in try-catch — they throw in incognito/private browsing (Safari, Firefox).\n\n## Reference\n\n- [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)\n","frontend/.claude/skills/react-best-practices/rules/js-index-maps.md":"---\ntitle: Build Index Maps for Repeated Lookups\nimpact: LOW-MEDIUM\nimpactDescription: 1M ops to 2K ops\ntags: javascript, map, indexing, optimization, performance\n---\n\n## Build Index Maps for Repeated Lookups\n\nMultiple `.find()` calls by the same key should use a Map.\n\n**Incorrect (O(n) per lookup):**\n\n```typescript\nfunction processOrders(orders: Order[], users: User[]) {\n  return orders.map(order => ({\n    ...order,\n    user: users.find(u => u.id === order.userId)\n  }))\n}\n```\n\n**Correct (O(1) per lookup):**\n\n```typescript\nfunction processOrders(orders: Order[], users: User[]) {\n  const userById = new Map(users.map(u => [u.id, u]))\n\n  return orders.map(order => ({\n    ...order,\n    user: userById.get(order.userId)\n  }))\n}\n```\n\nBuild map once (O(n)), then all lookups are O(1).\nFor 1000 orders × 1000 users: 1M ops → 2K ops.\n","frontend/.claude/skills/react-best-practices/rules/js-length-check-first.md":"---\ntitle: Early Length Check for Array Comparisons\nimpact: MEDIUM-HIGH\nimpactDescription: avoids expensive operations when lengths differ\ntags: javascript, arrays, performance, optimization, comparison\n---\n\n## Early Length Check for Array Comparisons\n\nWhen comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.\n\nIn real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).\n\n**Incorrect (always runs expensive comparison):**\n\n```typescript\nfunction hasChanges(current: string[], original: string[]) {\n  // Always sorts and joins, even when lengths differ\n  return current.sort().join() !== original.sort().join()\n}\n```\n\nTwo O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.\n\n**Correct (O(1) length check first):**\n\n```typescript\nfunction hasChanges(current: string[], original: string[]) {\n  // Early return if lengths differ\n  if (current.length !== original.length) {\n    return true\n  }\n  // Only sort when lengths match\n  const currentSorted = current.toSorted()\n  const originalSorted = original.toSorted()\n  for (let i = 0; i < currentSorted.length; i++) {\n    if (currentSorted[i] !== originalSorted[i]) {\n      return true\n    }\n  }\n  return false\n}\n```\n\nThis new approach is more efficient because:\n- It avoids the overhead of sorting and joining the arrays when lengths differ\n- It avoids consuming memory for the joined strings (especially important for large arrays)\n- It avoids mutating the original arrays\n- It returns early when a difference is found\n","frontend/.claude/skills/react-best-practices/rules/js-tosorted-immutable.md":"---\ntitle: Use toSorted() Instead of sort() for Immutability\nimpact: MEDIUM-HIGH\nimpactDescription: prevents mutation bugs in React state\ntags: javascript, arrays, immutability, react, state, mutation\n---\n\n## Use toSorted() Instead of sort() for Immutability\n\n`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.\n\n**Incorrect (mutates original array):**\n\n```typescript\nfunction UserList({ users }: { users: User[] }) {\n  // Mutates the users prop array!\n  const sorted = useMemo(\n    () => users.sort((a, b) => a.name.localeCompare(b.name)),\n    [users]\n  )\n  return <div>{sorted.map(renderUser)}</div>\n}\n```\n\n**Correct (creates new array):**\n\n```typescript\nfunction UserList({ users }: { users: User[] }) {\n  // Creates new sorted array, original unchanged\n  const sorted = useMemo(\n    () => users.toSorted((a, b) => a.name.localeCompare(b.name)),\n    [users]\n  )\n  return <div>{sorted.map(renderUser)}</div>\n}\n```\n\n**Why this matters in React:**\n\n1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only\n2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior\n\n**Browser support (fallback for older browsers):**\n\n`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:\n\n```typescript\n// Fallback for older browsers\nconst sorted = [...items].sort((a, b) => a.value - b.value)\n```\n\n**Other immutable array methods:**\n\n- `.toSorted()` - immutable sort\n- `.toReversed()` - immutable reverse\n- `.toSpliced()` - immutable splice\n- `.with()` - immutable element replacement\n","frontend/.claude/skills/react-best-practices/rules/rendering-activity.md":"---\ntitle: Use Activity Component for Show/Hide\nimpact: MEDIUM\nimpactDescription: preserves state/DOM\ntags: rendering, activity, visibility, state-preservation\n---\n\n## Use Activity Component for Show/Hide\n\nUse React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.\n\n**Usage:**\n\n```tsx\nimport { Activity } from 'react'\n\nfunction Dropdown({ isOpen }: Props) {\n  return (\n    <Activity mode={isOpen ? 'visible' : 'hidden'}>\n      <ExpensiveMenu />\n    </Activity>\n  )\n}\n```\n\nAvoids expensive re-renders and state loss.\n","frontend/.claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md":"---\ntitle: Animate SVG Wrapper Instead of SVG Element\nimpact: LOW\nimpactDescription: enables hardware acceleration\ntags: rendering, svg, css, animation, performance\n---\n\n## Animate SVG Wrapper Instead of SVG Element\n\nMany browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.\n\n**Incorrect (animating SVG directly - no hardware acceleration):**\n\n```tsx\nfunction LoadingSpinner() {\n  return (\n    <svg \n      className=\"animate-spin\"\n      width=\"24\" \n      height=\"24\" \n      viewBox=\"0 0 24 24\"\n    >\n      <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" />\n    </svg>\n  )\n}\n```\n\n**Correct (animating wrapper div - hardware accelerated):**\n\n```tsx\nfunction LoadingSpinner() {\n  return (\n    <div className=\"animate-spin\">\n      <svg \n        width=\"24\" \n        height=\"24\" \n        viewBox=\"0 0 24 24\"\n      >\n        <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" />\n      </svg>\n    </div>\n  )\n}\n```\n\nThis applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.\n","frontend/.claude/skills/react-best-practices/rules/rendering-conditional-render.md":"---\ntitle: Use Explicit Conditional Rendering\nimpact: LOW\nimpactDescription: prevents rendering 0 or NaN\ntags: rendering, conditional, jsx, falsy-values\n---\n\n## Use Explicit Conditional Rendering\n\nUse explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.\n\n**Incorrect (renders \"0\" when count is 0):**\n\n```tsx\nfunction Badge({ count }: { count: number }) {\n  return (\n    <div>\n      {count && <span className=\"badge\">{count}</span>}\n    </div>\n  )\n}\n\n// When count = 0, renders: <div>0</div>\n// When count = 5, renders: <div><span class=\"badge\">5</span></div>\n```\n\n**Correct (renders nothing when count is 0):**\n\n```tsx\nfunction Badge({ count }: { count: number }) {\n  return (\n    <div>\n      {count > 0 ? <span className=\"badge\">{count}</span> : null}\n    </div>\n  )\n}\n\n// When count = 0, renders: <div></div>\n// When count = 5, renders: <div><span class=\"badge\">5</span></div>\n```\n","frontend/.claude/skills/react-best-practices/rules/rendering-content-visibility.md":"---\ntitle: CSS content-visibility for Long Lists\nimpact: HIGH\nimpactDescription: faster initial render\ntags: rendering, css, content-visibility, long-lists\n---\n\n## CSS content-visibility for Long Lists\n\nApply `content-visibility: auto` to defer off-screen rendering.\n\n**CSS:**\n\n```css\n.message-item {\n  content-visibility: auto;\n  contain-intrinsic-size: 0 80px;\n}\n```\n\n**Example:**\n\n```tsx\nfunction MessageList({ messages }: { messages: Message[] }) {\n  return (\n    <div className=\"overflow-y-auto h-screen\">\n      {messages.map(msg => (\n        <div key={msg.id} className=\"message-item\">\n          <Avatar user={msg.author} />\n          <div>{msg.content}</div>\n        </div>\n      ))}\n    </div>\n  )\n}\n```\n\nFor 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).\n","frontend/.claude/skills/react-best-practices/rules/rendering-hoist-jsx.md":"---\ntitle: Hoist Static JSX Elements\nimpact: LOW\nimpactDescription: avoids re-creation\ntags: rendering, jsx, static, optimization\n---\n\n## Hoist Static JSX Elements\n\nExtract static JSX outside components to avoid re-creation.\n\n**Incorrect (recreates element every render):**\n\n```tsx\nfunction LoadingSkeleton() {\n  return <div className=\"animate-pulse h-20 bg-surface-strong\" />\n}\n\nfunction Container() {\n  return (\n    <div>\n      {loading && <LoadingSkeleton />}\n    </div>\n  )\n}\n```\n\n**Correct (reuses same element):**\n\n```tsx\nconst loadingSkeleton = (\n  <div className=\"animate-pulse h-20 bg-surface-strong\" />\n)\n\nfunction Container() {\n  return (\n    <div>\n      {loading && loadingSkeleton}\n    </div>\n  )\n}\n```\n\nThis is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.\n\n**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.\n","frontend/.claude/skills/react-best-practices/rules/rendering-svg-precision.md":"---\ntitle: Optimize SVG Precision\nimpact: LOW\nimpactDescription: reduces file size\ntags: rendering, svg, optimization, svgo\n---\n\n## Optimize SVG Precision\n\nReduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.\n\n**Incorrect (excessive precision):**\n\n```svg\n<path d=\"M 10.293847 20.847362 L 30.938472 40.192837\" />\n```\n\n**Correct (1 decimal place):**\n\n```svg\n<path d=\"M 10.3 20.8 L 30.9 40.2\" />\n```\n\n**Automate with SVGO:**\n\n```bash\nnpx svgo --precision=1 --multipass icon.svg\n```\n","frontend/.claude/skills/react-best-practices/rules/rerender-defer-reads.md":"---\ntitle: Defer State Reads to Usage Point\nimpact: MEDIUM\nimpactDescription: avoids unnecessary subscriptions\ntags: rerender, URLSearchParams, localStorage, optimization\n---\n\n## Defer State Reads to Usage Point\n\nDon't subscribe to dynamic state (URL params, localStorage) if you only read it inside callbacks.\n\n**Incorrect (subscribes to URL param changes, re-renders on every change):**\n\n```tsx\nfunction ShareButton({ chatId }: { chatId: string }) {\n  // Reading URL params at render time creates a subscription\n  const ref = new URLSearchParams(window.location.search).get('ref')\n\n  const handleShare = () => {\n    shareChat(chatId, { ref })\n  }\n\n  return <button onClick={handleShare}>Share</button>\n}\n```\n\n**Correct (reads on demand inside callback, no subscription):**\n\n```tsx\nfunction ShareButton({ chatId }: { chatId: string }) {\n  const handleShare = () => {\n    const params = new URLSearchParams(window.location.search)\n    const ref = params.get('ref')\n    shareChat(chatId, { ref })\n  }\n\n  return <button onClick={handleShare}>Share</button>\n}\n```\n\nSame principle applies to localStorage, sessionStorage, and cookies — read inside callbacks rather than at render time.\n","frontend/.claude/skills/react-best-practices/rules/rerender-dependencies.md":"---\ntitle: Narrow Effect Dependencies\nimpact: LOW\nimpactDescription: minimizes effect re-runs\ntags: rerender, useEffect, dependencies, optimization\n---\n\n## Narrow Effect Dependencies\n\nSpecify primitive dependencies instead of objects to minimize effect re-runs.\n\n**Incorrect (re-runs on any user field change):**\n\n```tsx\nuseEffect(() => {\n  console.log(user.id)\n}, [user])\n```\n\n**Correct (re-runs only when id changes):**\n\n```tsx\nuseEffect(() => {\n  console.log(user.id)\n}, [user.id])\n```\n\n**For derived state, compute outside effect:**\n\n```tsx\n// Incorrect: runs on width=767, 766, 765...\nuseEffect(() => {\n  if (width < 768) {\n    enableMobileMode()\n  }\n}, [width])\n\n// Correct: runs only on boolean transition\nconst isMobile = width < 768\nuseEffect(() => {\n  if (isMobile) {\n    enableMobileMode()\n  }\n}, [isMobile])\n```\n","frontend/.claude/skills/react-best-practices/rules/rerender-derived-state.md":"---\ntitle: Subscribe to Derived State\nimpact: MEDIUM\nimpactDescription: reduces re-render frequency\ntags: rerender, derived-state, media-query, optimization\n---\n\n## Subscribe to Derived State\n\nSubscribe to derived boolean state instead of continuous values to reduce re-render frequency.\n\n**Incorrect (re-renders on every pixel change):**\n\n```tsx\nfunction Sidebar() {\n  const width = useWindowWidth()  // updates continuously\n  const isMobile = width < 768\n  return <nav className={isMobile ? 'mobile' : 'desktop'} />\n}\n```\n\n**Correct (re-renders only when boolean changes):**\n\n```tsx\nfunction Sidebar() {\n  const isMobile = useMediaQuery('(max-width: 767px)')\n  return <nav className={isMobile ? 'mobile' : 'desktop'} />\n}\n```\n","frontend/.claude/skills/react-best-practices/rules/rerender-functional-setstate.md":"---\ntitle: Use Functional setState Updates\nimpact: MEDIUM\nimpactDescription: prevents stale closures and unnecessary callback recreations\ntags: react, hooks, useState, useCallback, callbacks, closures\n---\n\n## Use Functional setState Updates\n\nWhen updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.\n\n**Incorrect (requires state as dependency):**\n\n```tsx\nfunction TodoList() {\n  const [items, setItems] = useState(initialItems)\n  \n  // Callback must depend on items, recreated on every items change\n  const addItems = useCallback((newItems: Item[]) => {\n    setItems([...items, ...newItems])\n  }, [items])  // ❌ items dependency causes recreations\n  \n  // Risk of stale closure if dependency is forgotten\n  const removeItem = useCallback((id: string) => {\n    setItems(items.filter(item => item.id !== id))\n  }, [])  // ❌ Missing items dependency - will use stale items!\n  \n  return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />\n}\n```\n\nThe first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.\n\n**Correct (stable callbacks, no stale closures):**\n\n```tsx\nfunction TodoList() {\n  const [items, setItems] = useState(initialItems)\n  \n  // Stable callback, never recreated\n  const addItems = useCallback((newItems: Item[]) => {\n    setItems(curr => [...curr, ...newItems])\n  }, [])  // ✅ No dependencies needed\n  \n  // Always uses latest state, no stale closure risk\n  const removeItem = useCallback((id: string) => {\n    setItems(curr => curr.filter(item => item.id !== id))\n  }, [])  // ✅ Safe and stable\n  \n  return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />\n}\n```\n\n**Benefits:**\n\n1. **Stable callback references** - Callbacks don't need to be recreated when state changes\n2. **No stale closures** - Always operates on the latest state value\n3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks\n4. **Prevents bugs** - Eliminates the most common source of React closure bugs\n\n**When to use functional updates:**\n\n- Any setState that depends on the current state value\n- Inside useCallback/useMemo when state is needed\n- Event handlers that reference state\n- Async operations that update state\n\n**When direct updates are fine:**\n\n- Setting state to a static value: `setCount(0)`\n- Setting state from props/arguments only: `setName(newName)`\n- State doesn't depend on previous value\n\n**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.\n","frontend/.claude/skills/react-best-practices/rules/rerender-lazy-state-init.md":"---\ntitle: Use Lazy State Initialization\nimpact: MEDIUM\nimpactDescription: wasted computation on every render\ntags: react, hooks, useState, performance, initialization\n---\n\n## Use Lazy State Initialization\n\nPass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.\n\n**Incorrect (runs on every render):**\n\n```tsx\nfunction FilteredList({ items }: { items: Item[] }) {\n  // buildSearchIndex() runs on EVERY render, even after initialization\n  const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))\n  const [query, setQuery] = useState('')\n  \n  // When query changes, buildSearchIndex runs again unnecessarily\n  return <SearchResults index={searchIndex} query={query} />\n}\n\nfunction UserProfile() {\n  // JSON.parse runs on every render\n  const [settings, setSettings] = useState(\n    JSON.parse(localStorage.getItem('settings') || '{}')\n  )\n  \n  return <SettingsForm settings={settings} onChange={setSettings} />\n}\n```\n\n**Correct (runs only once):**\n\n```tsx\nfunction FilteredList({ items }: { items: Item[] }) {\n  // buildSearchIndex() runs ONLY on initial render\n  const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))\n  const [query, setQuery] = useState('')\n  \n  return <SearchResults index={searchIndex} query={query} />\n}\n\nfunction UserProfile() {\n  // JSON.parse runs only on initial render\n  const [settings, setSettings] = useState(() => {\n    const stored = localStorage.getItem('settings')\n    return stored ? JSON.parse(stored) : {}\n  })\n  \n  return <SettingsForm settings={settings} onChange={setSettings} />\n}\n```\n\nUse lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.\n\nFor simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.\n","frontend/.claude/skills/react-best-practices/rules/rerender-memo.md":"---\ntitle: Extract to Memoized Components\nimpact: MEDIUM\nimpactDescription: enables early returns\ntags: rerender, memo, useMemo, optimization\n---\n\n## Extract to Memoized Components\n\nExtract expensive work into memoized components to enable early returns before computation.\n\n**Incorrect (computes avatar even when loading):**\n\n```tsx\nfunction Profile({ user, loading }: Props) {\n  const avatar = useMemo(() => {\n    const id = computeAvatarId(user)\n    return <Avatar id={id} />\n  }, [user])\n\n  if (loading) return <Skeleton />\n  return <div>{avatar}</div>\n}\n```\n\n**Correct (skips computation when loading):**\n\n```tsx\nconst UserAvatar = memo(function UserAvatar({ user }: { user: User }) {\n  const id = useMemo(() => computeAvatarId(user), [user])\n  return <Avatar id={id} />\n})\n\nfunction Profile({ user, loading }: Props) {\n  if (loading) return <Skeleton />\n  return (\n    <div>\n      <UserAvatar user={user} />\n    </div>\n  )\n}\n```\n\n**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.\n","frontend/.claude/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Freact-best-practices%2Frules%2Frerender-simple-expression-in-memo.md -->","frontend/.claude/skills/react-best-practices/rules/rerender-transitions.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Freact-best-practices%2Frules%2Frerender-transitions.md -->","frontend/.claude/skills/react-best-practices/rules/use-effect.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Freact-best-practices%2Frules%2Fuse-effect.md -->","frontend/.claude/skills/security-scan/SKILL.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fsecurity-scan%2FSKILL.md -->","frontend/.claude/skills/security-scan/rules/_sections.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fsecurity-scan%2Frules%2F_sections.md -->","frontend/.claude/skills/security-scan/rules/_template.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fsecurity-scan%2Frules%2F_template.md -->","frontend/.claude/skills/security-scan/rules/vuln-direct-deps.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fsecurity-scan%2Frules%2Fvuln-direct-deps.md -->","frontend/.claude/skills/security-scan/rules/vuln-transitive-deps.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fsecurity-scan%2Frules%2Fvuln-transitive-deps.md -->","frontend/.claude/skills/state-management/SKILL.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fstate-management%2FSKILL.md -->","frontend/.claude/skills/state-management/rules/use-react-query-for-server.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fstate-management%2Frules%2Fuse-react-query-for-server.md -->","frontend/.claude/skills/state-management/rules/use-zustand-persist.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fstate-management%2Frules%2Fuse-zustand-persist.md -->","frontend/.claude/skills/state-management/rules/use-zustand-selectors.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fstate-management%2Frules%2Fuse-zustand-selectors.md -->","frontend/.claude/skills/tanstack-router-migration/SKILL.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftanstack-router-migration%2FSKILL.md -->","frontend/.claude/skills/tanstack-router-migration/references/build-configuration.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftanstack-router-migration%2Freferences%2Fbuild-configuration.md -->","frontend/.claude/skills/tanstack-router-migration/references/incremental-migration.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftanstack-router-migration%2Freferences%2Fincremental-migration.md -->","frontend/.claude/skills/tanstack-router-migration/references/migration-patterns.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftanstack-router-migration%2Freferences%2Fmigration-patterns.md -->","frontend/.claude/skills/tanstack-router-migration/references/route-templates.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftanstack-router-migration%2Freferences%2Froute-templates.md -->","frontend/.claude/skills/testing/SKILL.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2FSKILL.md -->","frontend/.claude/skills/testing/rules/mock-transport.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2Frules%2Fmock-transport.md -->","frontend/.claude/skills/testing/rules/no-ui-rendering-tests.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2Frules%2Fno-ui-rendering-tests.md -->","frontend/.claude/skills/testing/rules/test-mock-patterns.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2Frules%2Ftest-mock-patterns.md -->","frontend/.claude/skills/testing/rules/test-unit-vs-integration.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2Frules%2Ftest-unit-vs-integration.md -->","frontend/.claude/skills/testing/rules/test-zustand-stores.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2Frules%2Ftest-zustand-stores.md -->","frontend/.claude/skills/ui-development/SKILL.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2FSKILL.md -->","frontend/.claude/skills/ui-development/rules/icon-system.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Ficon-system.md -->","frontend/.claude/skills/ui-development/rules/registry-upgrade.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Fregistry-upgrade.md -->","frontend/.claude/skills/ui-development/rules/style-no-margin-on-registry.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Fstyle-no-margin-on-registry.md -->","frontend/.claude/skills/ui-development/rules/style-use-tokens.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Fstyle-use-tokens.md -->","frontend/.claude/skills/ui-development/rules/style-use-variants.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Fstyle-use-variants.md -->","frontend/.claude/skills/ui-development/rules/use-ui-registry.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Fuse-ui-registry.md -->","frontend/.claude/skills/web-design-guidelines/SKILL.md":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fweb-design-guidelines%2FSKILL.md -->","frontend/AGENTS.md":"# Redpanda Console Frontend\n\nReact 19 · Bun · Rsbuild\n\n## Critical Rules\n\n- New code MUST use Registry components (`src/components/redpanda-ui/`), react-hook-form + Zod, Bun unit tests, Rstest integration/browser/E2E tests, and Connect Query\n- See [no-legacy](.claude/skills/code-standards/rules/no-legacy.md) for prohibited patterns\n- `src/protogen/` is generated — DO NOT EDIT\n\n## Commands\n\nAlways use `bun run <script>` for project tools. Never use `bunx`, `npx`, or run tools directly.\n\n| Command | Purpose |\n|---------|---------|\n| `bun start` | Dev server :3000 |\n| `bun run build` | Production build |\n| `bun run test` | Unit, integration, federation and browser checks |\n| `bun run test:unit` | `.test.ts` (Bun) |\n| `bun run test:integration` | `.test.tsx` (happy-dom) |\n| `bun run lint` | Biome/Ultracite linter |\n| `bun run type:check` | TypeScript |\n\n## Verify Changes\n\nAfter changes: `bun run type:check && bun run lint && bun run test`\n\nClassify tests by behavior before choosing filenames. See [test guide](tests/README.md) for full-stack variants, fixtures, coverage and CI.\n\n## Directory Structure\n\n```\nsrc/\n├── components/\n│   ├── pages/          # Feature pages\n│   └── redpanda-ui/    # UI Registry (USE THIS)\n├── react-query/        # Connect Query hooks\n├── hooks/              # Custom hooks\n├── utils/              # Utilities\n└── protogen/           # Generated (DO NOT EDIT)\n```\n\n## Plan Mode\n\n- Extremely concise plans. Sacrifice grammar for concision.\n- End each plan with unresolved questions, if any.\n\n## Compaction\n\nWhen compacting, preserve: modified file list, test commands, and error messages.\n","frontend/CLAUDE.md":"@AGENTS.md\n",".cursorrules (kubestellar-console.mdc)":"---\ndescription: \"KubeStellar Console — Multi-cluster Kubernetes dashboard development rules\"\nglobs: **/*.tsx, **/*.ts, **/*.go, web/src/**/*.ts, web/src/**/*.tsx, pkg/**/*.go, cmd/**/*.go\nalwaysApply: false\n---\n# KubeStellar Console Development Rules\n\n## Project Structure\n- Frontend: React + TypeScript in `/web/`\n- Backend: Go (Fiber v2) in root directory\n- Build: `cd web && npm run build && npm run lint` before every commit\n- Cards: Dashboard card components in `web/src/components/cards/`\n- Hooks: Data fetching hooks in `web/src/hooks/`\n\n## Card Development\n- All data fetching MUST go through `useCache`/`useCached*` hooks\n- Always destructure and pass `isDemoData` and `isRefreshing` to `useCardLoadingState()`\n- Never use demo data during loading: `isDemoFallback && !isLoading`\n- Hook ordering: `useCardLoadingState` AFTER hooks providing `isDemoData`\n\n## Array Safety\n- NEVER call `.join()`, `.map()`, `.filter()`, `.forEach()`, `for...of` on values that might be undefined\n- Always guard: `(data || []).map(...)` or `(data || []).join(', ')`\n\n## No Magic Numbers\n- Numeric literals should be named constants, except trivial literals (`0`, `1`, `-1`) in clear local contexts\n- Use constants from `lib/constants/` (time.ts, network.ts, ui.ts)\n\n## Styling\n- Tailwind CSS with `cn()` utility for merging classNames\n- Never use raw hex colors — use semantic Tailwind classes (`text-foreground`, `bg-primary`, `bg-card`)\n- Status colors: `text-green-400`/`bg-green-500/10` (success), `text-yellow-400`/`bg-yellow-500/10` (warning), `text-red-400`/`bg-red-500/10` (error), `text-cyan-400`/`bg-cyan-500/10` (info) — these map to the design system's semantic status tokens and are the only permitted palette classes\n\n## Internationalization\n- All user-facing strings use `t()` from `react-i18next`\n- Keys in `web/src/locales/en/` JSON files\n- Never use raw strings for UI text\n\n## Go Backend\n- Fiber v2 handlers: `func(c *fiber.Ctx) error`\n- Use `fiber.NewError(statusCode, message)` for errors\n- Always `make([]T, 0)` not `var x []T` (nil → null in JSON)\n- Use `log/slog` for structured logging\n- Multi-cluster queries use goroutines + sync.WaitGroup\n\n## Cluster Deduplication\n- Always use `DeduplicatedClusters()` when iterating clusters\n- Multiple kubeconfig contexts can point to same physical cluster\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Redpanda Console\n\nGo backend (`backend/`) + React 19 single-page app (`frontend/`) sharing a protobuf API\ncontract (`proto/`). Builds and tooling are driven by [Task](https://taskfile.dev/) from the\nrepository root.\n\n## Which rules apply where\n\nRead the nearest file to what you are changing. The nested files own their area; this file\nonly routes.\n\n| You are changing | Read |\n|---|---|\n| `backend/**` | [backend/AGENTS.md](backend/AGENTS.md) |\n| `frontend/**` | [frontend/AGENTS.md](frontend/AGENTS.md) |\n| `proto/**` | this file, plus `buf.yaml` and `taskfiles/proto.yaml` |\n| root config, `taskfiles/**`, `.github/**` | this file |\n| commit messages, PR description, review | [.github/copilot-instructions.md](.github/copilot-instructions.md) and [CONTRIBUTING.md](CONTRIBUTING.md) |\n\n## Commands\n\nBackend and proto commands run **from the repository root** via `task`. The `./taskw` wrapper\npins the Go toolchain and helper binaries under `build/`.\n\n```bash\ntask backend:fmt            # format\ntask backend:lint           # golangci-lint\ntask backend:test-unit      # unit tests\ntask backend:test-integration   # unit + integration (needs Docker)\ntask backend:verify         # lint + generate + integration tests\ntask proto:lint             # buf lint\ntask proto:generate         # regenerate backend + frontend protogen\n```\n\nFrontend commands run **from `frontend/`** via `bun run <script>`.\n\n## Never edit (generated or vendored)\n\n- `backend/pkg/protogen/` and `frontend/src/protogen/` (regenerate with `task proto:generate`)\n- `frontend/src/routeTree.gen.ts` (TanStack Router)\n- `frontend/src/components/redpanda-ui/` (UI Registry, sourced from `redpanda-data/ui-registry`)\n- `build/` (pinned toolchain)\n\n## Enterprise boundary\n\nThis repository is the **OSS foundation**. `console-enterprise` imports\n`github.com/redpanda-data/console/backend` and extends it through the hooks system in\n`backend/pkg/api/hooks.go`, which lets it add interceptors, change routes, and replace service\nimplementations.\n\nFeatures that live behind those hooks (authentication, RBAC, secret management, shadow links,\npipelines, debug bundles) **cannot be exercised from this repository alone**. To test an OSS\nchange against enterprise, add a replace directive in `console-enterprise/backend/go.mod`:\n\n```go\nreplace github.com/redpanda-data/console/backend => /path/to/console/backend\n```\n\n## Proto changes\n\n`proto/` is published to the Buf Schema Registry as `buf.build/redpandadata/dataplane`. A\nchange here is an API contract change:\n\n1. Edit the `.proto` file.\n2. `task proto:lint && task proto:format`\n3. `task proto:generate` and commit the generated output. `proto-generate.yml` fails the PR if\n   generated code is out of date.\n4. Breaking-change detection runs in CI. Some alpha packages are exempt; check the `ignore`\n   list in `buf.yaml` before assuming yours is. Everything else is wire-compatible-only.\n","isInternal":false,"tokens":719,"sizeBytes":2905},{"name":"AGENTS.md","path":"backend/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/backend/AGENTS.md","title":"backend - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Redpanda Console Backend\n\nGo HTTP API server for managing and debugging Kafka/Redpanda workloads. Serves a legacy REST\nAPI under `/api` and a Connect RPC API re-exposed as JSON through gRPC-Gateway under\nversioned paths.\n\nModule: `github.com/redpanda-data/console/backend`. See [../AGENTS.md](../AGENTS.md) for\nrepo-wide rules.\n\n**How to use this file.** It tells you the rules you cannot infer and where to look for the\nrest. It is not a specification. Before relying on a specific, read the source: the linter\nconfig for lint rules, the Taskfile for commands, `routes.go` for what is wired, and the\npackage you are editing for its own patterns. When this file and the code disagree, the code\nwins and this file needs a fix.\n\n## Hard rules\n\nNon-obvious and enforced. Everything else the linter will tell you.\n\n**Errors.** Construct Connect errors through `apierrors`\n([pkg/api/connect/errors/](pkg/api/connect/errors/)), never `connect.NewError` directly, or\nyou lose the error details the helper always attaches. That package also holds converters for\nKafka, Schema Registry, and Redpanda Admin API errors; check for one before writing your own.\nLegacy REST handlers use a different model, `*rest.Error` with `rest.SendRESTError`.\n\n**Logging.** `log/slog` only, and `sloglint` is strict:\n\n- No global logger. Take a `*slog.Logger` as a constructor parameter and store it.\n- Attributes only: `slog.String(\"topic_name\", name)`, never loose key/value pairs.\n- snake_case keys, lowercase messages, and `\"error\"` rather than `\"err\"`.\n- Sub-loggers via `loggerpkg.Named(parent, \"topic_service\")`.\n\n**Doc comments.** `revive` runs with `enable-all-rules: true`, so every exported identifier\nneeds a doc comment.\n\n**License header.** Every new `.go` file starts with the BSL block:\n\n```go\n// Copyright 2026 Redpanda Data, Inc.\n//\n// Use of this software is governed by the Business Source License\n// included in the file licenses/BSL.md\n//\n// As of the Change Date specified in that file, in accordance with\n// the Business Source License, use of this software will be governed\n// by the Apache License, Version 2.0\n```\n\n**Imports.** Three blocks: standard library, third-party, then\n`github.com/redpanda-data/console/backend`. `task backend:fmt` sorts them.\n\n**Generated code.** Never hand-edit `pkg/protogen/`. Run `task proto:generate` from the\nrepository root.\n\nComplexity limits are enforced too, but you will hit those as lint failures rather than\nneeding to plan for them. The full linter set is in [.golangci.yaml](.golangci.yaml).\n\n## Comments\n\nKeep comments to a minimum. Less is more.\n\n- Doc comments on exported identifiers are required, as above. One line is enough.\n- Do not restate the code. `// increment the offset` above `offset++` is noise.\n- No banner comments (`// --- helpers ---`), no labelling the obvious.\n- Comment the **why**: a non-obvious constraint, a protocol quirk, a deliberate deviation, a\n  reason an ordering matters. Link the ticket or upstream issue if there is one.\n- Prefer a better name or a small extracted function over a paragraph of explanation.\n- No commented-out code. `TODO`/`FIXME` must name a ticket or an owner.\n\nDo not add comments to code you touched just to show what changed. That goes in the commit\nmessage.\n\n## Commands\n\nFrom the **repository root**. `task --list` has the rest.\n\n```bash\ntask backend:fmt                # format\ntask backend:lint               # golangci-lint\ntask backend:test-unit          # unit tests\ntask backend:test-integration   # unit + integration, needs Docker\ntask backend:verify             # lint, generate, integration tests\ntask backend:generate           # go generate, proto, format\n```\n\n`CLI_ARGS` is forwarded: `task backend:test-integration -- ./pkg/api/...`.\n\nTo invoke `go` directly, run it **from `backend/`**, since the commands are module-relative:\n\n```bash\ngo test ./pkg/serde/... -run TestJsonSerde\ngo test --tags=integration ./pkg/api/connect/integration/... -run TestSuite/TestTopicV1\n```\n\nRun the server from `backend/`, with a local cluster from `docs/local/`\n(`docker compose up -d`):\n\n```bash\nKAFKA_BROKERS=localhost:9092 SERVEFRONTEND=false SERVER_LISTENPORT=9090 go run cmd/api/main.go\n```\n\n**Before pushing:** `task backend:fmt && task backend:lint && task backend:test-unit`, then\n`task backend:test-integration` if you touched `pkg/api/` or `pkg/console/`.\n\n## Where things are\n\nOnly `cmd/` and `pkg/`. No `internal/`.\n\n| Path | Holds |\n|---|---|\n| `cmd/api/` | the single binary |\n| `pkg/api/` | HTTP and gRPC layer. `api.go` wires dependencies, `routes.go` mounts everything, `hooks.go` is the enterprise extension contract |\n| `pkg/api/connect/service/<domain>/<version>/` | Connect service implementations |\n| `pkg/console/` | core business logic behind the `Servicer` interface |\n| `pkg/factory/` | Kafka, Redpanda Admin, and Schema Registry client factories, consumed as interfaces |\n| `pkg/serde/` | auto-detecting serialization for Kafka messages |\n| `pkg/config/` | koanf config, one file per sub-struct |\n| `pkg/logger/` | slog setup with functional options |\n| `pkg/testutil/` | testcontainers helpers; image tags come from `test-images.json` at the repo root |\n| `pkg/protogen/` | generated, do not edit |\n\n`routes.go` is the map. It shows which services exist, which API versions are mounted, and\nwhich are OSS placeholders that enterprise replaces. Read it rather than assuming.\n\n## Conventions\n\n- Wrap errors with context: `fmt.Errorf(\"failed to describe topic: %w\", err)`. Compare with\n  `errors.Is`/`errors.As`, never `==` or string matching.\n- `ctx context.Context` is the first parameter, and never stored in a struct.\n- Accept interfaces, return concrete types. Assert conformance at package level:\n  `var _ dataplanev1connect.TopicServiceHandler = (*Service)(nil)`.\n- Constructors are `NewX(cfg, logger, collaborators...)`, dropping any the service does not\n  need. No setters.\n- A service package splits by role: `service.go` for the RPCs, `mapper.go` for proto\n  conversion, `defaulter.go` for request defaults, `util.go` for error translation. The\n  package is named for the domain even inside a `v1/` directory, so imports are aliased\n  (`topicsvcv1`).\n- `pkg/console` puts one exported operation per file, named in snake_case after the operation.\n- Config sub-structs implement `SetDefaults`, `RegisterFlags`, and `Validate` as needed. Do\n  not add an empty method to complete the set. Read `LoadConfig` in\n  [pkg/config/config.go](pkg/config/config.go) before changing precedence; the order of\n  defaults, flags, file, and env is load-bearing and not what you would guess.\n- Wiring is manual, no DI framework.\n\n## Enterprise boundary\n\nThis is the OSS foundation. `console-enterprise` imports it and extends it through the `Hooks`\ncontract in [pkg/api/hooks.go](pkg/api/hooks.go), which lets it add interceptors, replace OSS\nservice implementations, register additional services, and supply license and feature-flag\ninformation.\n\nChanging a hook signature breaks console-enterprise. Call it out in the PR description.\n\nThere is a stale duplicate of these declarations in `pkg/api/hooks/`. Nothing imports it;\ndo not add to it.\n\n## Testing\n\n- Table-driven is the house pattern: a slice of cases with a `name` and a `validateFn`\n  closure, run through `t.Run(tc.name, ...)`. See\n  [topic/v1/mapper_test.go](pkg/api/connect/service/topic/v1/mapper_test.go).\n- testify: `require` where a failure makes later assertions meaningless, `assert` for value\n  comparisons. Both in one test is normal.\n- Tests live in the same package as the code (`package topic`, not `topic_test`).\n- Unit tests carry no build tag. Integration tests carry `//go:build integration` and the\n  `_integration_test.go` suffix, except in `pkg/api/connect/integration/` where the tag alone\n  marks them.\n- Integration suites use testcontainers-go and testify/suite. See\n  [integration/api_suite_test.go](pkg/api/connect/integration/api_suite_test.go).\n- Mocks are gomock, from a `//go:generate mockgen` directive next to the interface.\n  Regenerate with `task backend:generate`.\n","isInternal":false,"tokens":1988,"sizeBytes":8080},{"name":"AGENTS.md","path":"frontend/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/AGENTS.md","title":"frontend - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Redpanda Console Frontend\n\nReact 19 · Bun · Rsbuild\n\n## Critical Rules\n\n- New code MUST use Registry components (`src/components/redpanda-ui/`), react-hook-form + Zod, Bun unit tests, Rstest integration/browser/E2E tests, and Connect Query\n- See [no-legacy](.claude/skills/code-standards/rules/no-legacy.md) for prohibited patterns\n- `src/protogen/` is generated — DO NOT EDIT\n\n## Commands\n\nAlways use `bun run <script>` for project tools. Never use `bunx`, `npx`, or run tools directly.\n\n| Command | Purpose |\n|---------|---------|\n| `bun start` | Dev server :3000 |\n| `bun run build` | Production build |\n| `bun run test` | Unit, integration, federation and browser checks |\n| `bun run test:unit` | `.test.ts` (Bun) |\n| `bun run test:integration` | `.test.tsx` (happy-dom) |\n| `bun run lint` | Biome/Ultracite linter |\n| `bun run type:check` | TypeScript |\n\n## Verify Changes\n\nAfter changes: `bun run type:check && bun run lint && bun run test`\n\nClassify tests by behavior before choosing filenames. See [test guide](tests/README.md) for full-stack variants, fixtures, coverage and CI.\n\n## Directory Structure\n\n```\nsrc/\n├── components/\n│   ├── pages/          # Feature pages\n│   └── redpanda-ui/    # UI Registry (USE THIS)\n├── react-query/        # Connect Query hooks\n├── hooks/              # Custom hooks\n├── utils/              # Utilities\n└── protogen/           # Generated (DO NOT EDIT)\n```\n\n## Plan Mode\n\n- Extremely concise plans. Sacrifice grammar for concision.\n- End each plan with unresolved questions, if any.\n\n## Compaction\n\nWhen compacting, preserve: modified file list, test commands, and error messages.\n","isInternal":false,"tokens":444,"sizeBytes":1678},{"name":"CLAUDE.md","path":"backend/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/backend/CLAUDE.md","title":"backend - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"@AGENTS.md\n","isInternal":false,"tokens":5,"sizeBytes":11},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"@AGENTS.md\n","isInternal":false,"tokens":5,"sizeBytes":11},{"name":"CLAUDE.md","path":"frontend/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/CLAUDE.md","title":"frontend - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"@AGENTS.md\n","isInternal":false,"tokens":5,"sizeBytes":11},{"name":"copilot-instructions.md","path":".github/copilot-instructions.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/.github/copilot-instructions.md","title":"GitHub Copilot Instructions","category":"copilot-instructions","format":"markdown","content":"# Copilot instructions: Redpanda Console\n\nGo backend (`backend/`) + React 19 SPA (`frontend/`) sharing a protobuf contract (`proto/`).\nTask drives builds from the repository root.\n\n## Where the rules live\n\nArea rules are **not** repeated here. They live in `AGENTS.md` files and the nearest one to a\nchanged file wins:\n\n| Path | Governing file |\n|---|---|\n| `backend/**` | `backend/AGENTS.md` |\n| `frontend/**` | `frontend/AGENTS.md` |\n| `proto/**`, root config, `taskfiles/**` | `AGENTS.md` |\n| commits, PR description, review conduct | this file |\n\nWhen a review comment enforces an area rule, name the file it came from so the author can\nread the rule in context.\n\n## Commit messages\n\nFormat is `area[/detail]: short description`.\n\nexample:\n```\nbackend: resolve schema IDs in the topic's registry context\n\nBefore this change the message viewer always looked schemas up in the\ndefault context, so a topic bound to a named context returned a decode\nerror. Resolution now takes the topic's context, which also removes the\nspecial case in the sample handler.\n```\n\n- Subject: 50 characters or fewer, lowercase after the prefix, no trailing period.\n- Common areas: `backend`, `frontend`, `proto`, `topics`, `chore`, `deps`.\n- Body: wrap at 72 columns. Explain **why**, not what: the state being moved away from, why\n  it is no longer wanted, and how the new state addresses that. Then any non-obvious\n  technical detail.\n- A commit message must stand alone. No \"in the previous commit\", no references to meetings\n  or threads. That context goes in the PR description.\n- One logical change per commit, and every commit must build on its own so `git bisect`\n  works.\n\nParts of the history use Conventional Commits (`feat(frontend): ...`). `area:` is the\ndocumented standard in `CONTRIBUTING.md` and the form to use for new work.\n\n## Pull requests\n\n- Title follows the same `area: description` convention.\n- The description carries the high-level narrative that individual commits must not depend\n  on, plus examples of what's being added.\n- Fold review fixups back into the commits they modify before merge. Do not leave\n  \"address review feedback\" commits in the final history.\n- `git rebase --keep-base` keeps the diff for reviewers minimal on a force-push.\n- Call out explicitly when a change touches `backend/pkg/api/hooks.go` or `proto/`. Those are\n  the contract surfaces `console-enterprise` builds on, and a signature change there breaks\n  it. (`backend/pkg/api/hooks/` is a stale duplicate that nothing imports.)\n\n## Comments\n\nRepo-wide: keep comments to a minimum. Less is more. Full rules in\n[backend/AGENTS.md](../backend/AGENTS.md#comments).\n\n- Comment the **why**, never the what. A non-obvious constraint, a protocol quirk, a\n  deliberate deviation. Not `// increment the offset` above `offset++`.\n- Doc comments on exported Go identifiers are mandatory (`revive` runs with all rules on) and\n  are the exception to this, not a license to annotate everything.\n- No banner comments, no commented-out code, no comments that exist to describe what the diff\n  changed. That belongs in the commit message.\n- `TODO`/`FIXME` must name a ticket or an owner.\n- Prefer a better name or a small extracted function over a paragraph explaining the code.\n\nWhen reviewing, flag added comments that restate the code and suggest deleting them. This is\nthe most common form of bloat in AI-assisted pull requests.\n\n## Review priorities\n\nIn order:\n\n1. **Correctness.** Wrong behavior, unhandled errors, swallowed errors, nil dereferences,\n   races, resource leaks, missing `ctx` propagation.\n2. **The area rules** in the relevant `AGENTS.md`, especially the \"Hard rules\" section of\n   `backend/AGENTS.md` and the \"Critical Rules\" of `frontend/AGENTS.md`.\n3. **Contract and compatibility.** Proto changes that are not wire-compatible, hook signature\n   changes, API version placement, generated output not regenerated.\n4. **Tests.** New behavior needs a test. Backend prefers table-driven with `t.Run`; frontend\n   splits `.test.ts` (unit, node) from `.test.tsx` (integration, happy-dom).\n5. **Noise.** Comments that restate the code, commented-out code, and dead scaffolding. See\n   Comments above.\n\n## Do not flag\n\n- Anything under a generated or vendored path: `backend/pkg/protogen/`,\n  `frontend/src/protogen/`, `frontend/src/routeTree.gen.ts`,\n  `frontend/src/components/redpanda-ui/`, `build/`. Changes there are either generated output\n  or upstream registry content.\n- Formatting and style nits already enforced by tooling. `golangci-lint` 2.10 (37 linters,\n  `revive` with all rules on, `sloglint`, `gci`) and Biome/Ultracite gate those in CI, and a\n  duplicate review comment adds noise without adding information.\n- Line length, import order, or trailing whitespace. The formatters own those.\n\n## What CI enforces\n\n| Workflow | Gate |\n|---|---|\n| `backend-lint-test.yml` | `golangci-lint` + unit and integration tests |\n| `frontend-verify.yml` | Biome/Ultracite (with a dirty-tree check), `ultracite doctor`, type check, build, tests |\n| `buf.yml` | proto lint, format, and breaking-change detection |\n| `proto-generate.yml` | fails if generated proto output is not committed |\n| `frontend-react-doctor.yml` | React Doctor on frontend changes |\n| `frontend-ui-audit.yml` | advisory only: registry drift, off-token colors, ad-hoc utility classes |\n| `repository-dispatch.yml` | triggers the `console-enterprise` build after verify |\n\nPrefer pointing at the gate that will catch an issue over restating what the gate already\nsays.\n","isInternal":false,"tokens":1340,"sizeBytes":5529},{"name":"test-unit-vs-integration.md","path":"frontend/.claude/rules/test-unit-vs-integration.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/rules/test-unit-vs-integration.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"src/**/*.test.{ts,tsx}\"\n---\n\n# Test classification\n\nClassify what the test exercises before choosing its filename:\n\n- Pure logic/mocked module contracts: `.test.ts`, Bun (`bun run test:unit`).\n- React, DOM, hooks, state/transport integration: `.test.tsx`, Rstest + Happy DOM.\n- Configuration/subprocess integration: `.integration.test.ts`, Rstest.\n- Native browser and full running-app journeys: `.spec.ts`, Rstest + Playwright.\n\nSee [test guide](../../tests/README.md) for commands and fixtures. Keep full-stack journeys on Testcontainers; mocked bundle tests do not replace them.\n","frontmatter":{"paths":["src/**/*.test.{ts,tsx}"]},"isInternal":false,"tokens":143,"sizeBytes":598},{"name":"api-invalidate-cache.md","path":"frontend/.claude/skills/api-patterns/rules/api-invalidate-cache.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/api-patterns/rules/api-invalidate-cache.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Invalidate Cache After Mutations\nimpact: HIGH\nimpactDescription: Stale cache after mutations causes UI to show outdated data\ntags: cache, invalidation, mutation, react-query\n---\n\n# Invalidate Cache After Mutations (HIGH)\n\n## Explanation\n\nAfter mutations (create, update, delete), invalidate related queries to ensure the UI reflects the latest server state. Without invalidation, users see stale data until they manually refresh.\n\n## Incorrect\n\n```tsx\n// No invalidation after mutation\nconst mutation = useMutation(updateCluster, {\n  onSuccess: () => {\n    toast.success('Cluster updated');\n    // UI still shows old data!\n  },\n});\n```\n\n```tsx\n// Invalidating wrong query key\nconst mutation = useMutation(updateCluster, {\n  onSuccess: () => {\n    queryClient.invalidateQueries({ queryKey: ['clusters'] }); // Wrong key\n  },\n});\n```\n\n## Correct\n\n```tsx\nimport { useMutation } from '@connectrpc/connect-query';\nimport { useQueryClient } from '@tanstack/react-query';\n\nconst queryClient = useQueryClient();\n\nconst mutation = useMutation(updateCluster, {\n  onSuccess: () => {\n    // Invalidate specific query\n    queryClient.invalidateQueries({\n      queryKey: ['getCluster', clusterId],\n    });\n\n    // Or invalidate all cluster queries\n    queryClient.invalidateQueries({\n      queryKey: ['getClusters'],\n    });\n\n    toast.success('Cluster updated');\n  },\n});\n```\n\n## Optimistic Updates\n\nFor instant UI feedback:\n\n```tsx\nconst mutation = useMutation(updateCluster, {\n  onMutate: async (newData) => {\n    // Cancel outgoing refetches\n    await queryClient.cancelQueries({ queryKey: ['getCluster', id] });\n\n    // Snapshot previous value\n    const previous = queryClient.getQueryData(['getCluster', id]);\n\n    // Optimistically update\n    queryClient.setQueryData(['getCluster', id], newData);\n\n    return { previous };\n  },\n  onError: (err, newData, context) => {\n    // Rollback on error\n    queryClient.setQueryData(['getCluster', id], context?.previous);\n  },\n  onSettled: () => {\n    // Always refetch after mutation settles\n    queryClient.invalidateQueries({ queryKey: ['getCluster', id] });\n  },\n});\n```\n\n## Reference\n\n- [Query Invalidation](https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation)\n- [Optimistic Updates](https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates)\n","frontmatter":{"title":"Invalidate Cache After Mutations","impact":"HIGH","impactDescription":"Stale cache after mutations causes UI to show outdated data","tags":"cache, invalidation, mutation, react-query"},"isInternal":false,"tokens":533,"sizeBytes":2345},{"name":"api-toast-errors.md","path":"frontend/.claude/skills/api-patterns/rules/api-toast-errors.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/api-patterns/rules/api-toast-errors.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Toast Error Messages\nimpact: HIGH\nimpactDescription: Consistent error messages help users understand and recover from failures\ntags: error-handling, toast, ux, mutations\n---\n\n# Toast Error Messages (HIGH)\n\n## Explanation\n\nUse `formatToastErrorMessage` utility for consistent, user-friendly error messages in mutation `onError` handlers. This provides a standard format that includes the action, entity, and error details.\n\n## Incorrect\n\n```tsx\n// Generic error message\nconst mutation = useMutation(deleteCluster, {\n  onError: () => {\n    toast.error('Something went wrong');\n  },\n});\n\n// Raw error object\nconst mutation = useMutation(deleteCluster, {\n  onError: (error) => {\n    toast.error(error.message); // May be technical/unhelpful\n  },\n});\n\n// No error handling\nconst mutation = useMutation(deleteCluster, {\n  onSuccess: () => toast.success('Deleted'),\n  // Missing onError!\n});\n```\n\n## Correct\n\n```tsx\nimport { formatToastErrorMessage } from 'utils/toast.utils';\n\nconst mutation = useMutation(deleteCluster, {\n  onSuccess: () => {\n    toast.success('Cluster deleted');\n  },\n  onError: (error) => {\n    toast.error(formatToastErrorMessage({\n      error: {\n        reason: error.message,\n        internalCode: error.code,\n      },\n      action: 'delete',\n      entity: 'cluster',\n    }));\n  },\n});\n\n// Output: \"Failed to delete cluster due to: reason (code: 123)\"\n```\n\n## Standard Actions\n\n| Action | Use For |\n|--------|---------|\n| `create` | POST / create operations |\n| `update` | PUT / PATCH operations |\n| `delete` | DELETE operations |\n| `fetch` | GET operations (rare) |\n\n## Error Boundaries for Render Errors\n\n```tsx\nimport * as Sentry from '@sentry/react';\n\n<Sentry.ErrorBoundary fallback={<ErrorFallback />}>\n  <ClusterDashboard />\n</Sentry.ErrorBoundary>\n```\n\n## Reference\n\n- `src/utils/toast.utils.ts` - Toast utility implementation\n","frontmatter":{"title":"Toast Error Messages","impact":"HIGH","impactDescription":"Consistent error messages help users understand and recover from failures","tags":"error-handling, toast, ux, mutations"},"isInternal":false,"tokens":425,"sizeBytes":1862},{"name":"protobuf-no-edit.md","path":"frontend/.claude/skills/api-patterns/rules/protobuf-no-edit.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/api-patterns/rules/protobuf-no-edit.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Protobuf No Edit\nimpact: CRITICAL\nimpactDescription: Never edit files in protogen/ - they are auto-generated\ntags: protobuf, generated, code-gen\n---\n\n## Protobuf No Edit\n\n**Impact:** CRITICAL - Never edit files in protogen/ - they are auto-generated\n\nThe `/src/protogen/` directory contains auto-generated TypeScript from protobuf schemas. Any manual edits will be lost on regeneration and may cause type mismatches.\n\n**Incorrect:**\n```typescript\n// WRONG: Editing generated file\n// src/protogen/user_pb.ts\nexport interface User {\n  id: string;\n  name: string;\n  customField: string; // WRONG: Manual addition\n}\n```\n\n**Correct:**\n```typescript\n// CORRECT: Extend types outside protogen\n// src/types/user.ts\nimport { User as ProtoUser } from \"protogen/user_pb\";\n\nexport interface ExtendedUser extends ProtoUser {\n  customField: string;\n}\n\n// CORRECT: Transform data in hooks/utils\n// src/hooks/useUser.ts\nconst { data } = useQuery(getUser, { id });\n\nconst extendedUser = useMemo(() => ({\n  ...data,\n  displayName: `${data?.firstName} ${data?.lastName}`,\n}), [data]);\n```\n\n## Regenerating Protos\n\nWhen proto schemas change upstream:\n\n```bash\n# From repository root (not frontend/)\ntask proto:generate\n```\n\n## Finding Generated Files\n\n```\nsrc/protogen/\n├── *_pb.ts           # Message types\n├── *_connect.ts      # Service definitions\n└── *_connectquery.ts # Connect Query hooks\n```\n\n## Common Imports\n\n```typescript\n// Message types\nimport { User, CreateUserRequest } from \"protogen/user_pb\";\n\n// Service methods\nimport { UserService } from \"protogen/user_connect\";\n\n// Query hooks\nimport { getUser, listUsers, createUser } from \"protogen/user-UserService_connectquery\";\n```\n","frontmatter":{"title":"Protobuf No Edit","impact":"CRITICAL","impactDescription":"Never edit files in protogen/ - they are auto-generated","tags":"protobuf, generated, code-gen"},"isInternal":false,"tokens":404,"sizeBytes":1702},{"name":"use-connect-query.md","path":"frontend/.claude/skills/api-patterns/rules/use-connect-query.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/api-patterns/rules/use-connect-query.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Use Connect Query\nimpact: CRITICAL\nimpactDescription: Consistent API patterns enable caching, refetching, and error handling\ntags: connect-query, react-query, api, grpc\n---\n\n# Use Connect Query (CRITICAL)\n\n## Explanation\n\nAlways use Connect Query hooks (`useQuery`, `useMutation`) for API calls. These integrate with React Query for caching, background refetching, and optimistic updates. Never use raw fetch or manual state management for server data.\n\n## Incorrect\n\n```tsx\n// Manual fetch with useState\nfunction ClusterList() {\n  const [clusters, setClusters] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    fetch('/api/clusters')\n      .then(res => res.json())\n      .then(data => {\n        setClusters(data);\n        setLoading(false);\n      });\n  }, []);\n}\n```\n\n```tsx\n// Missing enabled flag\nconst { data } = useQuery(\n  getClusters,\n  { organizationId: undefined } // Will error!\n);\n```\n\n## Correct\n\n```tsx\nimport { useQuery } from '@connectrpc/connect-query';\nimport { getClusters } from 'proto/frontend/controlplane/v1/cluster-ClusterService_connectquery';\n\nfunction ClusterList({ organizationId }: Props) {\n  const {\n    data: clusters,\n    isLoading,\n    error,\n    refetch,\n  } = useQuery(\n    getClusters,\n    { organizationId },\n    {\n      staleTime: 1000 * 60 * 5, // 5 minutes\n      enabled: !!organizationId, // Only fetch when ID exists\n    }\n  );\n\n  if (isLoading) return <Skeleton />;\n  if (error) return <ErrorDisplay error={error} />;\n\n  return <List items={clusters} />;\n}\n```\n\n## Common Options\n\n| Option | Purpose |\n|--------|---------|\n| `enabled` | Conditional fetching |\n| `staleTime` | Cache duration before refetch |\n| `refetchInterval` | Polling interval |\n| `refetchOnWindowFocus` | Refetch when tab focused |\n\n## Reference\n\n- [Connect Query Docs](https://connectrpc.com/docs/web/query/getting-started)\n- [TanStack Query Options](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery)\n- See also: [use-react-query-for-server](../../state-management/rules/use-react-query-for-server.md) — when to use React Query vs Zustand\n","frontmatter":{"title":"Use Connect Query","impact":"CRITICAL","impactDescription":"Consistent API patterns enable caching, refetching, and error handling","tags":"connect-query, react-query, api, grpc"},"isInternal":false,"tokens":509,"sizeBytes":2130},{"name":"SKILL.md","path":"frontend/.claude/skills/api-patterns/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/api-patterns/SKILL.md","title":"api-patterns","category":"anthropic-skill","format":"markdown","content":"---\nname: api-patterns\ndescription: Connect Query patterns for API calls. Use when working with mutations, queries, or data fetching.\n---\n\n# API Patterns\n\nMake API calls with Connect Query and handle responses properly.\n\n## Activation Conditions\n\n- Making API calls\n- Using Connect Query hooks\n- Cache invalidation\n- Mutations and optimistic updates\n- Toast notifications for errors\n\n## Quick Reference\n\n| Action | Rule |\n|--------|------|\n| Fetch data | `use-connect-query.md` |\n| After mutation | `api-invalidate-cache.md` |\n| Handle errors | `api-toast-errors.md` (use `formatToastErrorMessage` in onError) |\n| Protobuf files | `protobuf-no-edit.md` |\n\n## Key Locations\n\n| Location | Purpose |\n|----------|---------|\n| `/src/react-query/` | Connect Query hooks |\n| `/src/protogen/` | Generated protos (DO NOT EDIT) |\n\nRegenerate protos: `task proto:generate` (from repo root)\n\n## Rules\n\nSee `rules/` directory for detailed guidance on queries, mutations, cache invalidation, and error handling.\n","frontmatter":{"name":"api-patterns","description":"Connect Query patterns for API calls. Use when working with mutations, queries, or data fetching."},"isInternal":false,"tokens":232,"sizeBytes":998},{"name":"async-await-promises.md","path":"frontend/.claude/skills/code-standards/rules/async-await-promises.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/code-standards/rules/async-await-promises.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Async/Await Over Promise Chains\nimpact: HIGH\nimpactDescription: async/await is more readable and makes error handling explicit\ntags: async, await, promises, javascript\n---\n\n# Async/Await Over Promise Chains (HIGH)\n\n## Explanation\n\nUse `async/await` instead of `.then()` chains. Async/await is more readable, makes error handling explicit, and avoids callback hell. Never leave promises floating without handling.\n\n## Incorrect\n\n```typescript\n// Promise chains\nfunction loadData() {\n  fetchUser()\n    .then(user => fetchPosts(user.id))\n    .then(posts => processPosts(posts))\n    .catch(error => handleError(error));\n}\n\n// Floating promise (no await)\nfunction save() {\n  saveToDatabase(data); // Promise returned but not awaited!\n}\n\n// Async in Promise executor\nnew Promise(async (resolve) => {\n  const data = await fetch(url);\n  resolve(data);\n});\n\n// Mixing patterns\nasync function mixed() {\n  return fetch(url).then(res => res.json());\n}\n```\n\n## Correct\n\n```typescript\n// async/await\nasync function loadData() {\n  try {\n    const user = await fetchUser();\n    const posts = await fetchPosts(user.id);\n    return processPosts(posts);\n  } catch (error) {\n    handleError(error);\n  }\n}\n\n// Always await or return promises\nasync function save() {\n  await saveToDatabase(data);\n}\n\n// Parallel execution when needed\nasync function loadAll() {\n  const [users, posts] = await Promise.all([\n    fetchUsers(),\n    fetchPosts(),\n  ]);\n  return { users, posts };\n}\n\n// Consistent async/await\nasync function fetchJson(url: string) {\n  const response = await fetch(url);\n  return response.json();\n}\n```\n\n## Error Handling\n\n```typescript\n// Explicit try/catch\nasync function riskyOperation() {\n  try {\n    const result = await mightFail();\n    return result;\n  } catch (error) {\n    // Log and rethrow or handle\n    console.error('Operation failed:', error);\n    throw error;\n  }\n}\n\n// Error boundaries for React\n<ErrorBoundary fallback={<ErrorFallback />}>\n  <AsyncComponent />\n</ErrorBoundary>\n```\n\n## Reference\n\n- [MDN async/await](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Promises)\n","frontmatter":{"title":"Async/Await Over Promise Chains","impact":"HIGH","impactDescription":"async/await is more readable and makes error handling explicit","tags":"async, await, promises, javascript"},"isInternal":false,"tokens":484,"sizeBytes":2114},{"name":"no-legacy.md","path":"frontend/.claude/skills/code-standards/rules/no-legacy.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/code-standards/rules/no-legacy.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: No Legacy Patterns\nimpact: CRITICAL\nimpactDescription: Never use legacy patterns - they block migration efforts\ntags: legacy, migration, mobx, yup\n---\n\n## No Legacy Patterns\n\n**Impact:** CRITICAL - Never use legacy patterns - they block migration efforts\n\nThe codebase is actively migrating away from legacy libraries. Using them in new code blocks migration and creates tech debt.\n\n## Prohibited Patterns\n\n| Legacy | Modern Alternative |\n|--------|-------------------|\n| MobX, decorators | Zustand for global state |\n| Class components | Functional components + hooks |\n| Yup validation | Zod |\n| `any` type | `unknown` or specific types |\n| Jest | Rstest |\n\n## Common Violations\n\n**MobX:**\n```typescript\n// WRONG\nimport { makeAutoObservable, observable, action } from \"mobx\";\nimport { observer } from \"mobx-react-lite\";\n\n@observer\nclass Store {\n  @observable users = [];\n}\n```\n\n```typescript\n// CORRECT\nimport { create } from \"zustand\";\n\nconst useStore = create<State>((set) => ({\n  users: [],\n  addUser: (user) => set((s) => ({ users: [...s.users, user] })),\n}));\n```\n\n**Class Components:**\n```tsx\n// WRONG\nclass UserProfile extends React.Component<Props, State> {\n  componentDidMount() { /* ... */ }\n  render() { return <div />; }\n}\n```\n\n```tsx\n// CORRECT\nconst UserProfile = ({ userId }: Props) => {\n  const { data } = useQuery(getUser, { id: userId });\n  return <Card>{data?.name}</Card>;\n};\n```\n\n## Migration Guidance\n\nWhen modifying files with legacy patterns:\n1. **Don't add** new legacy imports\n2. **Consider migrating** the component if changes are significant\n3. **Keep changes minimal** if just fixing bugs\n4. **New features** must use modern patterns\n\n### MobX → React State: Check Side-Effects\n\nWhen converting observables to React state, automatic behaviors must become explicit:\n- MobX observable arrays auto-update UI when mutated\n- React state requires explicit `setState()` calls\n- Clearing, resetting, or initializing state needs manual handling\n\n**Checklist**: List all observable mutations → add equivalent `setState` calls.\n\n## Legacy Locations\n\n- MobX stores: `src/state/` (do not add new files)\n\n## See Also\n\n- [use-ui-registry](../../ui-development/rules/use-ui-registry.md) — Registry component usage and installation\n","frontmatter":{"title":"No Legacy Patterns","impact":"CRITICAL","impactDescription":"Never use legacy patterns - they block migration efforts","tags":"legacy, migration, mobx, yup"},"isInternal":false,"tokens":530,"sizeBytes":2263},{"name":"react-functional-only.md","path":"frontend/.claude/skills/code-standards/rules/react-functional-only.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/code-standards/rules/react-functional-only.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Functional Components Only\nimpact: HIGH\nimpactDescription: Class components are deprecated patterns that make code harder to test and compose\ntags: react, components, functional, hooks\n---\n\n# Functional Components Only (HIGH)\n\n## Explanation\n\nAlways use functional components with hooks. Class components are a legacy pattern that makes code harder to test, compose, and optimize. Functional components are the modern React standard.\n\n## Incorrect\n\n```tsx\n// Class component\nclass UserProfile extends React.Component<Props, State> {\n  state = { loading: true };\n\n  componentDidMount() {\n    this.fetchUser();\n  }\n\n  render() {\n    return <div>{this.state.loading ? 'Loading...' : this.props.name}</div>;\n  }\n}\n\n// Nested component definition (causes remounting)\nfunction Parent() {\n  function Child() {\n    return <div>Child</div>;\n  }\n  return <Child />; // Child remounts every render!\n}\n\n// Conditional hooks\nfunction Component({ enabled }) {\n  if (enabled) {\n    const [state, setState] = useState(); // Rules of Hooks violation\n  }\n}\n```\n\n## Correct\n\n```tsx\n// Functional component\nexport function UserProfile({ userId }: Props) {\n  const [loading, setLoading] = useState(true);\n  const [user, setUser] = useState<User | null>(null);\n\n  useEffect(() => {\n    fetchUser(userId).then(data => {\n      setUser(data);\n      setLoading(false);\n    });\n  }, [userId]);\n\n  if (loading) return <Skeleton />;\n\n  return <div>{user?.name}</div>;\n}\n\n// Sibling components defined outside\nfunction Parent() {\n  return <Child />;\n}\n\nfunction Child() {\n  return <div>Child</div>;\n}\n\n// Hooks always at top level\nfunction Component({ enabled }: Props) {\n  const [state, setState] = useState<string | null>(null);\n\n  if (!enabled) return null;\n\n  return <div>{state}</div>;\n}\n```\n\n## Component Patterns\n\n```tsx\n// Named export (preferred)\nexport function FeatureComponent() { }\n\n// Colocated sub-components\nexport function Feature() {\n  return (\n    <div>\n      <FeatureHeader />\n      <FeatureContent />\n    </div>\n  );\n}\n\n// Keep sub-components in same file if small\nfunction FeatureHeader() { return <h1>Title</h1>; }\nfunction FeatureContent() { return <p>Content</p>; }\n```\n\n## Reference\n\n- [React Hooks](https://react.dev/reference/react/hooks)\n- [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks)\n","frontmatter":{"title":"Functional Components Only","impact":"HIGH","impactDescription":"Class components are deprecated patterns that make code harder to test and compose","tags":"react, components, functional, hooks"},"isInternal":false,"tokens":536,"sizeBytes":2317},{"name":"SKILL.md","path":"frontend/.claude/skills/code-standards/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/code-standards/SKILL.md","title":"code-standards","category":"anthropic-skill","format":"markdown","content":"---\nname: code-standards\ndescription: TypeScript, React, and JavaScript best practices enforced by Ultracite/Biome.\n---\n\n# Code Standards\n\nWrite code that is accessible, performant, type-safe, and maintainable.\n\n## Activation Conditions\n\n- Linting errors need fixing\n- Code formatting issues\n- Code review feedback\n- Questions about style guidelines\n\n## Quick Reference\n\n| Action | Rule |\n|--------|------|\n| Write components | `react-functional-only.md` |\n| Async code | `async-await-promises.md` |\n| Avoid legacy libs | `no-legacy.md` |\n\n`any` types and top-level regex are enforced by Biome (`noExplicitAny`, `useTopLevelRegex`).\n\n## Commands\n\n```bash\nbun x ultracite fix     # Format and fix\nbun x ultracite check   # Check for issues\nbun x ultracite doctor  # Diagnose setup\n```\n\n## Quick Fix\n\nMost issues are auto-fixed:\n\n```bash\nbun x ultracite fix\n```\n\n## Console Logging\n\n- **Never** use `console.log`, `console.debug`, or `console.info` in production code\n- Only use `console.error` and `console.warn` for actionable errors\n- For debug logging, wrap in `if (IsDev) { ... }`\n- Don't use `biome-ignore` to suppress console warnings - remove the logs instead\n\n## Rules\n\nSee `rules/` directory for detailed guidance.\n","frontmatter":{"name":"code-standards","description":"TypeScript, React, and JavaScript best practices enforced by Ultracite/Biome."},"isInternal":false,"tokens":312,"sizeBytes":1223},{"name":"container-setup.md","path":"frontend/.claude/skills/e2e-tester/references/container-setup.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/e2e-tester/references/container-setup.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Container setup reference\n\nThe maintained lifecycle lives in `tests/shared/global-setup.mjs`, shared by all four\nRstest variant configs. Read [Testcontainers details](../../../../tests/TESTCONTAINERS.md)\nand [test commands](../../../../tests/README.md) before changing it.\n\nBuild the frontend first. Setup copies its assets into the backend build context,\nbuilds an image, starts the configured brokers/services and backend, and waits for\nreadiness. The returned teardown owns live handles; `global-teardown.mjs` provides\nmanual crash recovery using the serialized state. Preserve failure cleanup.\n\nVariant metadata/configuration lives in `tests/test-variant-*/config/`. Images are\npinned in `test-images.json` at the repository root. Local single-variant runs remap host ports;\ncontainer-to-container addresses stay unchanged.\n\nEnterprise requires a separate backend checkout (`ENTERPRISE_BACKEND_DIR`). Global\nsetup authenticates through the real login form and stores browser state under the\ngitignored `playwright/.auth/` directory. Login-error specs override storage with an\nempty state. There is no HTTP Basic-auth replacement for application login.\n\nCI runs three OSS variants in `.github/workflows/frontend-verify.yml`. It installs\nChromium and Linux dependencies, downloads the same-run frontend build, and invokes\n`bun run e2e-test:variant <variant>`. Failure evidence is uploaded from `tests/.rstest/`.\nThe enterprise backend remains the responsibility of downstream enterprise CI.\n\nFor port conflicts, inspect the owner or use the dynamic-port wrapper. Never kill an\nunidentified developer process. For service failures, inspect Docker logs and the\nvariant state file rather than weakening waits or assertions.\n","isInternal":false,"tokens":347,"sizeBytes":1725},{"name":"failure-analysis.md","path":"frontend/.claude/skills/e2e-tester/references/failure-analysis.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/e2e-tester/references/failure-analysis.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Failure Analysis Reference\n\n## CLI and artifacts\n\nRun `bun run e2e-test:variant console list --summary` to collect tests without services.\nRun `bun run e2e-test:variant console topics/topic-list.spec.ts` to reproduce against the\nreal stack. Inspect `.rstest/` screenshots, first-retry videos and Playwright traces.\nEnterprise screenshots/videos remain disabled. Use native locators/DOM inspection for\nselector diagnosis; do not replace real E2E coverage with mocked component tests.\n\n## Common Failure Patterns\n\n### 1. Element Not Found\n\n```\nError: locator.click: Target closed\nError: Timeout 30000ms exceeded waiting for locator\n```\n\n**Analysis steps:**\n1. Check if element has correct testid/role\n2. Verify element is visible (not hidden/collapsed)\n3. Check for timing issues (element loads async)\n4. Look for dynamic content that changes selector\n\n**Fix:**\n```typescript\n// BAD: Element might not be loaded\nawait page.getByRole('button', { name: 'Create' }).click();\n\n// GOOD: Wait for element to be visible\nawait expect(page.getByRole('button', { name: 'Create' })).toBeVisible();\nawait page.getByRole('button', { name: 'Create' }).click();\n\n// BETTER: Add testid for stability\nawait page.getByTestId('create-button').click();\n```\n\n### 2. Selector Ambiguity\n\n```\nError: strict mode violation: locator('button') resolved to 3 elements\n```\n\n**Fix:**\n```typescript\n// BAD: Multiple \"Edit\" buttons on page\nawait page.getByRole('button', { name: 'Edit' }).click();\n\n// GOOD: More specific with testid\nawait page.getByTestId('schema-edit-compatibility-btn').click();\n\n// GOOD: Scope within container\nawait page.getByRole('region', { name: 'Schema Details' })\n          .getByRole('button', { name: 'Edit' }).click();\n```\n\n### 3. Timing/Race Conditions\n\n```\nError: expect(locator).toHaveText()\nExpected string: \"Success\"\nReceived string: \"Loading...\"\n```\n\n**Fix:**\n```typescript\n// BAD: Doesn't wait for state change\nawait page.getByRole('button', { name: 'Save' }).click();\nexpect(page.getByText('Success')).toBeVisible();\n\n// GOOD: Wait for the expected state\nawait page.getByRole('button', { name: 'Save' }).click();\nawait expect(page.getByText('Success')).toBeVisible({ timeout: 5000 });\n```\n\n### 4. Navigation Issues\n\n```\nError: page.goto: net::ERR_CONNECTION_REFUSED\n```\n\n**Fix:**\n```bash\n# Check containers are running\ndocker ps | grep console-backend\n\n# Check container logs\ndocker logs <container-id>\n\n# Verify port mapping\ncurl http://localhost:3000\n\n# Check testcontainer state file\ncat .testcontainers-state.json\n```\n\n## Systematic Failure Analysis Workflow\n\n1. **Get Test Results** — Run the relevant variant CLI and inspect console/artifact output\n2. **Analyze Error Patterns:**\n   - Selector not found -> Missing/wrong testid or element not visible\n   - Strict mode violation -> Need more specific selector\n   - Timeout -> Element loads async, need waitFor\n   - Connection refused -> Container/service not running\n3. **Find Missing Test IDs** — Inspect semantic roles and labels in the source and browser\n4. **Add Test IDs** — Read component, add `data-testid`, follow naming convention\n5. **Update Tests** — Replace brittle selectors with stable testids, add wait conditions\n6. **Verify Fixes** — Run specific test file, then full suite\n\n## Debugging Commands\n\n```bash\n# Check container logs\ndocker ps -a | grep console-backend\ndocker logs <container-id>\n\n# Check if services are accessible\ncurl http://localhost:3000\ncurl http://localhost:19092\n\n# Run with debug output\nDEBUG=pw:api bun run e2e-test\n\n# Keep containers running on failure\nTESTCONTAINERS_RYUK_DISABLED=true bun run e2e-test\n```\n\n**In-test debugging:**\n```typescript\nawait page.pause();  // Opens Playwright Inspector\nawait page.screenshot({ path: 'debug.png' });\nconsole.log(await page.content());\n```\n\n## Test Timeout Issues\n\n```typescript\n// Increase timeout for slow operations\ntest('slow operation', async ({ page }) => {\n  test.setTimeout(60000); // 60 seconds\n  await page.goto('/slow-page');\n  await expect(page.getByText('Loaded')).toBeVisible({ timeout: 30000 });\n});\n```\n","isInternal":false,"tokens":983,"sizeBytes":4072},{"name":"test-patterns.md","path":"frontend/.claude/skills/e2e-tester/references/test-patterns.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/e2e-tester/references/test-patterns.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Test Patterns Reference\n\n## Basic Test Structure\n\n```typescript\nimport { test, expect } from '../shared/e2e';\n\ntest.describe('Feature Name', () => {\n  test('user can complete workflow', async ({ page }) => {\n    await page.goto('/feature');\n    await page.getByRole('button', { name: 'Create' }).click();\n    await page.getByLabel('Name').fill('test-item');\n    await page.getByRole('button', { name: 'Submit' }).click();\n    await expect(page.getByText('Success')).toBeVisible();\n    await expect(page).toHaveURL(/\\/feature\\/test-item/);\n  });\n});\n```\n\n## Multi-Step Workflows\n\n```typescript\ntest('user creates, configures, and tests topic', async ({ page }) => {\n  // Step 1: Navigate and create\n  await page.goto('/topics');\n  await page.getByRole('button', { name: 'Create Topic' }).click();\n\n  // Step 2: Fill form\n  await page.getByLabel('Topic Name').fill('test-topic');\n  await page.getByLabel('Partitions').fill('3');\n  await page.getByRole('button', { name: 'Create' }).click();\n\n  // Step 3: Verify creation\n  await expect(page.getByText('Topic created successfully')).toBeVisible();\n  await expect(page).toHaveURL(/\\/topics\\/test-topic/);\n\n  // Step 4: Configure topic\n  await page.getByRole('button', { name: 'Configure' }).click();\n  await page.getByLabel('Retention Hours').fill('24');\n  await page.getByRole('button', { name: 'Save' }).click();\n\n  // Step 5: Verify configuration\n  await expect(page.getByText('Configuration saved')).toBeVisible();\n});\n```\n\n## Testing Forms\n\n```typescript\ntest('form validation works correctly', async ({ page }) => {\n  await page.goto('/create-topic');\n\n  // Submit empty form - should show errors\n  await page.getByRole('button', { name: 'Create' }).click();\n  await expect(page.getByText('Name is required')).toBeVisible();\n\n  // Fill valid data - should succeed\n  await page.getByLabel('Topic Name').fill('valid-topic');\n  await page.getByRole('button', { name: 'Create' }).click();\n  await expect(page.getByText('Success')).toBeVisible();\n});\n```\n\n## Testing Data Tables\n\n```typescript\ntest('user can filter and sort topics', async ({ page }) => {\n  await page.goto('/topics');\n\n  // Filter\n  await page.getByPlaceholder('Search topics').fill('test-');\n  await expect(page.getByRole('row')).toHaveCount(3); // Header + 2 results\n\n  // Sort\n  await page.getByRole('columnheader', { name: 'Name' }).click();\n  const firstRow = page.getByRole('row').nth(1);\n  await expect(firstRow).toContainText('test-topic-a');\n});\n```\n\n## API Interactions\n\n```typescript\ntest('creating topic triggers backend API', async ({ page }) => {\n  // Listen for API call\n  const apiPromise = page.waitForResponse(\n    resp => resp.url().includes('/api/topics') && resp.status() === 201\n  );\n\n  // Trigger action\n  await page.goto('/topics');\n  await page.getByRole('button', { name: 'Create Topic' }).click();\n  await page.getByLabel('Name').fill('api-test-topic');\n  await page.getByRole('button', { name: 'Create' }).click();\n\n  // Verify API was called\n  const response = await apiPromise;\n  const body = await response.json();\n  expect(body.name).toBe('api-test-topic');\n});\n```\n\n## Async Operations\n\n```typescript\n// Wait for specific condition\nawait expect(page.getByRole('status')).toHaveText('Ready');\n\n// Wait for navigation\nawait page.waitForURL('**/topics/my-topic');\n\n// Wait for API response\nawait page.waitForResponse(resp =>\n  resp.url().includes('/api/topics') && resp.status() === 200\n);\n\n// NEVER use fixed timeouts\n// await page.waitForTimeout(5000); // BAD\n```\n\n## Selectors Best Practices\n\n```typescript\n// GOOD: Role-based (accessible)\npage.getByRole('button', { name: 'Create Topic' })\npage.getByLabel('Topic Name')\npage.getByText('Success message')\n\n// GOOD: Test IDs when role isn't clear\npage.getByTestId('topic-list-item')\n\n// BAD: CSS selectors (brittle)\npage.locator('.btn-primary')\npage.locator('#topic-name-input')\n```\n\n## Adding Test IDs to Components\n\n```tsx\n// In React component\n<Button data-testid=\"create-topic-button\">Create Topic</Button>\n\n// In test\nawait page.getByTestId('create-topic-button').click();\n```\n\n**Where to Add:**\n1. Primary actions: Create, Save, Delete, Edit, Submit, Cancel buttons\n2. Navigation: Links to detail pages, breadcrumbs\n3. Forms: All input fields, selects, checkboxes, radio buttons\n4. Lists/Tables: Row identifiers, action buttons within rows\n5. Dialogs/Modals: Open/close buttons, form elements inside\n6. Search/Filter: Search inputs, filter dropdowns, clear buttons\n","isInternal":false,"tokens":1096,"sizeBytes":4470},{"name":"SKILL.md","path":"frontend/.claude/skills/e2e-tester/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/e2e-tester/SKILL.md","title":"e2e-tester","category":"anthropic-skill","format":"markdown","content":"---\nname: e2e-tester\ndescription: \"Write and run Rstest/Playwright E2E tests for Redpanda Console using testcontainers. Analyzes test failures, adds missing testids, and improves test stability. Use when user requests E2E tests, Playwright tests, integration tests, test failures, missing testids, or mentions 'test workflow', 'browser testing', 'end-to-end', or 'testcontainers'.\"\nallowed-tools: Read, Write, Edit, Bash, Glob, Grep, Task, mcp__ide__getDiagnostics\n---\n\n# E2E testing with Rstest, Playwright and Testcontainers\n\nWrite end-to-end tests using Playwright against a full Redpanda Console stack running in Docker containers via testcontainers.\n\n## When to Use This Skill\n\n- Testing 2+ step user journeys (login -> action -> verify)\n- Multi-page workflows\n- Browser automation with Playwright\n\n**NOT for:** Component unit tests -> use [testing](../testing/SKILL.md)\n\n## Critical Rules\n\n**ALWAYS:**\n- Run `bun run build` before running E2E tests (frontend assets required)\n- Use `testcontainers` API for container management (never manual `docker` commands in tests)\n- Use `page.getByRole()` and `page.getByLabel()` selectors (avoid CSS selectors)\n- Add `data-testid` attributes when semantic selectors aren't available\n- Run Rstest CLI commands and inspect `.rstest/` artifacts to analyze failures\n- Clean up test data using `afterEach` to call cleanup API endpoints\n\n**NEVER:**\n- Test UI component rendering (use unit/integration tests)\n- Use brittle CSS selectors like `.class-name` or `#id`\n- Use `force:true` on `.click()` or `waitForTimeout`\n- Hard-code wait times (use `waitFor` with conditions)\n- Leave containers running after test failures\n- Commit test screenshots to git\n\n## Commands\n\n```bash\nbun run build                # Build frontend (REQUIRED first!)\nbun run e2e-test:variant console  # OSS E2E\nbun run e2e-test:variant console-enterprise  # Requires ENTERPRISE_BACKEND_DIR\nbun run e2e-test:variant console --watch  # Rstest watcher\nbun run e2e-test:variant console topics/topic-creation.spec.ts\nbun run e2e-test  # All four variants, including enterprise\n```\n\n## Test Architecture\n\n**OSS Mode (`bun run e2e-test:variant console`):** Redpanda + Backend + OwlShop containers\n**Enterprise Mode (`bun run e2e-test:variant console-enterprise`):** Same + RBAC, SSO (requires `console-enterprise` repo)\n\nFile location: `tests/test-variant-<variant>/<feature>/*.spec.ts`.\nImport `test`/`expect` from `tests/shared/e2e.ts` or the variant fixtures.\nSee [runner contracts](../../../tests/README.md).\n\n## Selector Priority\n\n1. `getByRole()` - Best for accessibility\n2. `getByLabel()` - For form inputs\n3. `getByText()` - For content verification\n4. `getByTestId()` - When semantic selectors aren't clear\n5. CSS selectors - Avoid if possible\n\n## Test ID Naming\n\n- kebab-case: `data-testid=\"feature-action-element\"`\n- Specific: include feature name + action + element type\n- Dynamic: `data-testid={\\`item-delete-\\${id}\\`}`\n\n## References\n\n- [Container Setup](references/container-setup.md) — Testcontainer lifecycle, configs, CI setup\n- [Test Patterns](references/test-patterns.md) — Multi-step workflows, forms, tables, API testing\n- [Failure Analysis](references/failure-analysis.md) — Error patterns and CLI debugging\n\n## Output\n\nAfter completing work:\n1. Confirm frontend build succeeded\n2. Verify all E2E tests pass\n3. Note any new test IDs added to components\n4. Mention cleanup of test containers\n","frontmatter":{"name":"e2e-tester","description":"Write and run Rstest/Playwright E2E tests for Redpanda Console using testcontainers. Analyzes test failures, adds missing testids, and improves test stability. Use when user requests E2E tests, Playwright tests, integration tests, test failures, missing testids, or mentions 'test workflow', 'browser testing', 'end-to-end', or 'testcontainers'.","allowed-tools":"Read, Write, Edit, Bash, Glob, Grep, Task, mcp__ide__getDiagnostics","allowedTools":["Read,","Write,","Edit,","Bash,","Glob,","Grep,","Task,","mcp__ide__getDiagnostics"]},"allowedTools":["Read,","Write,","Edit,","Bash,","Glob,","Grep,","Task,","mcp__ide__getDiagnostics"],"isInternal":false,"tokens":851,"sizeBytes":3424},{"name":"common-patterns.md","path":"frontend/.claude/skills/form-refactorer/references/common-patterns.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/form-refactorer/references/common-patterns.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Common Form Patterns\n\nThis document provides patterns for common form scenarios using Field components.\n\n## Dynamic Fields\n\nForms with conditionally rendered fields based on user input.\n\n```tsx\nimport { useForm, useWatch } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { z } from \"zod\";\nimport {\n  FieldSet,\n  FieldLegend,\n  FieldGroup,\n  Field,\n  FieldLabel,\n  FieldError\n} from \"components/redpanda-ui/components/field\";\nimport { Select } from \"components/redpanda-ui/components/select\";\nimport { Input } from \"components/redpanda-ui/components/input\";\n\nconst schema = z.object({\n  authType: z.enum([\"none\", \"basic\", \"token\"]),\n  username: z.string().optional(),\n  password: z.string().optional(),\n  token: z.string().optional(),\n}).refine((data) => {\n  if (data.authType === \"basic\") {\n    return !!data.username && !!data.password;\n  }\n  if (data.authType === \"token\") {\n    return !!data.token;\n  }\n  return true;\n}, {\n  message: \"Authentication fields are required\",\n});\n\nexport const AuthForm = () => {\n  const { register, control, formState: { errors } } = useForm({\n    resolver: zodResolver(schema),\n  });\n\n  const authType = useWatch({ control, name: \"authType\" });\n\n  return (\n    <FieldSet>\n      <FieldLegend>Authentication</FieldLegend>\n      <FieldGroup>\n        <Field data-invalid={!!errors.authType}>\n          <FieldLabel htmlFor=\"authType\">Authentication Type</FieldLabel>\n          <Select id=\"authType\" {...register(\"authType\")}>\n            <option value=\"none\">None</option>\n            <option value=\"basic\">Basic Auth</option>\n            <option value=\"token\">Token</option>\n          </Select>\n          {errors.authType && <FieldError>{errors.authType.message}</FieldError>}\n        </Field>\n\n        {authType === \"basic\" && (\n          <>\n            <Field data-invalid={!!errors.username}>\n              <FieldLabel htmlFor=\"username\">Username</FieldLabel>\n              <Input id=\"username\" {...register(\"username\")} aria-invalid={!!errors.username} />\n              {errors.username && <FieldError>{errors.username.message}</FieldError>}\n            </Field>\n\n            <Field data-invalid={!!errors.password}>\n              <FieldLabel htmlFor=\"password\">Password</FieldLabel>\n              <Input id=\"password\" type=\"password\" {...register(\"password\")} aria-invalid={!!errors.password} />\n              {errors.password && <FieldError>{errors.password.message}</FieldError>}\n            </Field>\n          </>\n        )}\n\n        {authType === \"token\" && (\n          <Field data-invalid={!!errors.token}>\n            <FieldLabel htmlFor=\"token\">API Token</FieldLabel>\n            <Input id=\"token\" {...register(\"token\")} aria-invalid={!!errors.token} />\n            {errors.token && <FieldError>{errors.token.message}</FieldError>}\n          </Field>\n        )}\n      </FieldGroup>\n    </FieldSet>\n  );\n};\n```\n\n## Field Arrays (Repeatable Fields)\n\nForms with dynamically added/removed field groups.\n\n```tsx\nimport { useForm, useFieldArray } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { z } from \"zod\";\nimport {\n  FieldSet,\n  FieldLegend,\n  FieldGroup,\n  Field,\n  FieldLabel,\n  FieldError,\n  FieldSeparator\n} from \"components/redpanda-ui/components/field\";\nimport { Input } from \"components/redpanda-ui/components/input\";\nimport { Button } from \"components/redpanda-ui/components/button\";\n\nconst schema = z.object({\n  tags: z.array(z.object({\n    key: z.string().min(1, \"Key is required\"),\n    value: z.string().min(1, \"Value is required\"),\n  })).min(1, \"At least one tag is required\"),\n});\n\nexport const TagsForm = () => {\n  const { register, control, formState: { errors } } = useForm({\n    resolver: zodResolver(schema),\n    defaultValues: {\n      tags: [{ key: \"\", value: \"\" }],\n    },\n  });\n\n  const { fields, append, remove } = useFieldArray({\n    control,\n    name: \"tags\",\n  });\n\n  return (\n    <FieldSet>\n      <FieldLegend>Tags</FieldLegend>\n      <FieldGroup>\n        {fields.map((field, index) => (\n          <div key={field.id}>\n            {index > 0 && <FieldSeparator />}\n            <FieldGroup>\n              <Field data-invalid={!!errors.tags?.[index]?.key}>\n                <FieldLabel htmlFor={`tags.${index}.key`}>Key</FieldLabel>\n                <Input\n                  id={`tags.${index}.key`}\n                  {...register(`tags.${index}.key`)}\n                  aria-invalid={!!errors.tags?.[index]?.key}\n                />\n                {errors.tags?.[index]?.key && (\n                  <FieldError>{errors.tags[index].key.message}</FieldError>\n                )}\n              </Field>\n\n              <Field data-invalid={!!errors.tags?.[index]?.value}>\n                <FieldLabel htmlFor={`tags.${index}.value`}>Value</FieldLabel>\n                <Input\n                  id={`tags.${index}.value`}\n                  {...register(`tags.${index}.value`)}\n                  aria-invalid={!!errors.tags?.[index]?.value}\n                />\n                {errors.tags?.[index]?.value && (\n                  <FieldError>{errors.tags[index].value.message}</FieldError>\n                )}\n              </Field>\n\n              {fields.length > 1 && (\n                <Button\n                  type=\"button\"\n                  variant=\"destructive\"\n                  onClick={() => remove(index)}\n                >\n                  Remove Tag\n                </Button>\n              )}\n            </FieldGroup>\n          </div>\n        ))}\n\n        <Button\n          type=\"button\"\n          variant=\"secondary\"\n          onClick={() => append({ key: \"\", value: \"\" })}\n        >\n          Add Tag\n        </Button>\n      </FieldGroup>\n    </FieldSet>\n  );\n};\n```\n\n## Multi-Section Forms\n\nForms organized into multiple logical sections.\n\n```tsx\nimport {\n  FieldSet,\n  FieldLegend,\n  FieldDescription,\n  FieldGroup,\n  Field,\n  FieldLabel,\n  FieldSeparator,\n  FieldError\n} from \"components/redpanda-ui/components/field\";\nimport { Input } from \"components/redpanda-ui/components/input\";\nimport { Textarea } from \"components/redpanda-ui/components/textarea\";\nimport { Switch } from \"components/redpanda-ui/components/switch\";\n\nexport const ProfileSettingsForm = () => {\n  const { register, formState: { errors } } = useForm({\n    resolver: zodResolver(schema),\n  });\n\n  return (\n    <form>\n      {/* Personal Information Section */}\n      <FieldSet>\n        <FieldLegend>Personal Information</FieldLegend>\n        <FieldDescription>\n          This information will be displayed on your public profile.\n        </FieldDescription>\n        <FieldGroup>\n          <Field data-invalid={!!errors.name}>\n            <FieldLabel htmlFor=\"name\">Full Name</FieldLabel>\n            <Input id=\"name\" {...register(\"name\")} aria-invalid={!!errors.name} />\n            {errors.name && <FieldError>{errors.name.message}</FieldError>}\n          </Field>\n\n          <Field data-invalid={!!errors.bio}>\n            <FieldLabel htmlFor=\"bio\">Bio</FieldLabel>\n            <Textarea id=\"bio\" {...register(\"bio\")} aria-invalid={!!errors.bio} />\n            {errors.bio && <FieldError>{errors.bio.message}</FieldError>}\n          </Field>\n        </FieldGroup>\n      </FieldSet>\n\n      <FieldSeparator />\n\n      {/* Notification Preferences Section */}\n      <FieldSet>\n        <FieldLegend>Notification Preferences</FieldLegend>\n        <FieldDescription>\n          Manage how you receive notifications.\n        </FieldDescription>\n        <FieldGroup>\n          <Field orientation=\"horizontal\">\n            <Switch id=\"emailNotifications\" {...register(\"emailNotifications\")} />\n            <FieldContent>\n              <FieldLabel htmlFor=\"emailNotifications\">Email Notifications</FieldLabel>\n              <FieldDescription>Receive notifications via email</FieldDescription>\n            </FieldContent>\n          </Field>\n\n          <Field orientation=\"horizontal\">\n            <Switch id=\"pushNotifications\" {...register(\"pushNotifications\")} />\n            <FieldContent>\n              <FieldLabel htmlFor=\"pushNotifications\">Push Notifications</FieldLabel>\n              <FieldDescription>Receive push notifications in browser</FieldDescription>\n            </FieldContent>\n          </Field>\n        </FieldGroup>\n      </FieldSet>\n    </form>\n  );\n};\n```\n\n## Read-Only Fields\n\nDisplay form fields in a read-only state.\n\n```tsx\nimport {\n  Field,\n  FieldLabel,\n  FieldDescription\n} from \"components/redpanda-ui/components/field\";\nimport { Input } from \"components/redpanda-ui/components/input\";\n\nexport const ServerDetailsForm = ({ server }) => {\n  return (\n    <FieldGroup>\n      <Field>\n        <FieldLabel htmlFor=\"id\">Server ID</FieldLabel>\n        <Input id=\"id\" value={server.id} disabled />\n        <FieldDescription>Auto-generated unique identifier</FieldDescription>\n      </Field>\n\n      <Field>\n        <FieldLabel htmlFor=\"createdAt\">Created At</FieldLabel>\n        <Input id=\"createdAt\" value={server.createdAt} disabled />\n      </Field>\n    </FieldGroup>\n  );\n};\n```\n\n## Inline Form Actions\n\nForms with actions positioned within the field group.\n\n```tsx\nimport {\n  FieldSet,\n  FieldLegend,\n  FieldGroup,\n  Field,\n  FieldLabel,\n  FieldError\n} from \"components/redpanda-ui/components/field\";\nimport { Input } from \"components/redpanda-ui/components/input\";\nimport { Button } from \"components/redpanda-ui/components/button\";\n\nexport const InlineEditForm = () => {\n  const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm();\n\n  return (\n    <form onSubmit={handleSubmit(onSubmit)}>\n      <FieldSet>\n        <FieldLegend>Quick Edit</FieldLegend>\n        <FieldGroup>\n          <Field data-invalid={!!errors.name}>\n            <FieldLabel htmlFor=\"name\">Name</FieldLabel>\n            <div className=\"flex gap-2\">\n              <Input\n                id=\"name\"\n                {...register(\"name\")}\n                aria-invalid={!!errors.name}\n                className=\"flex-1\"\n              />\n              <Button type=\"submit\" disabled={isSubmitting}>\n                Save\n              </Button>\n            </div>\n            {errors.name && <FieldError>{errors.name.message}</FieldError>}\n          </Field>\n        </FieldGroup>\n      </FieldSet>\n    </form>\n  );\n};\n```\n\n## Common Zod Patterns\n\n```tsx\n// String validations\nz.string().min(1, \"Required\")\nz.string().email(\"Invalid email\")\nz.string().url(\"Invalid URL\")\nz.string().regex(/^[A-Z]/, \"Must start with capital\")\n\n// Number validations\nz.number().min(0, \"Must be positive\")\nz.number().max(100, \"Max 100\")\nz.number().int(\"Must be integer\")\n\n// Optional fields\nz.string().optional()\nz.string().nullable()\n\n// Arrays\nz.array(z.string()).min(1, \"At least one required\")\n\n// Objects\nz.object({\n  nested: z.string(),\n})\n\n// Conditional validation\nz.object({\n  type: z.enum([\"a\", \"b\"]),\n  value: z.string(),\n}).refine((data) => {\n  if (data.type === \"a\") return data.value.length > 5;\n  return true;\n}, \"Value must be longer than 5 for type A\")\n```\n\n## Common Pitfalls\n\n### Pitfall 1: Missing htmlFor/id Association\n\n```tsx\n// WRONG:\n<Field>\n  <FieldLabel>Email</FieldLabel>\n  <Input name=\"email\" />\n</Field>\n\n// CORRECT:\n<Field>\n  <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n  <Input id=\"email\" name=\"email\" />\n</Field>\n```\n\n### Pitfall 2: Missing aria-invalid\n\n```tsx\n// WRONG:\n<Field data-invalid={!!errors.email}>\n  <Input {...register(\"email\")} />\n</Field>\n\n// CORRECT:\n<Field data-invalid={!!errors.email}>\n  <Input {...register(\"email\")} aria-invalid={!!errors.email} />\n</Field>\n```\n\n### Pitfall 3: Conditional FieldError Without Check\n\n```tsx\n// WRONG:\n<FieldError>{errors.email.message}</FieldError>\n\n// CORRECT:\n{errors.email && <FieldError>{errors.email.message}</FieldError>}\n```\n\n### Pitfall 4: Using Legacy Form Components\n\n```tsx\n// WRONG:\nimport { Form } from \"components/redpanda-ui/components/form\";\n\n// CORRECT:\nimport { Field, FieldLabel } from \"components/redpanda-ui/components/field\";\n```\n\n### Pitfall 5: Forgetting FieldContent for Horizontal Fields\n\n```tsx\n// WRONG:\n<Field orientation=\"horizontal\">\n  <Switch id=\"notifications\" />\n  <FieldLabel htmlFor=\"notifications\">Enable</FieldLabel>\n  <FieldDescription>Receive updates</FieldDescription>\n</Field>\n\n// CORRECT:\n<Field orientation=\"horizontal\">\n  <Switch id=\"notifications\" />\n  <FieldContent>\n    <FieldLabel htmlFor=\"notifications\">Enable</FieldLabel>\n    <FieldDescription>Receive updates</FieldDescription>\n  </FieldContent>\n</Field>\n```\n","isInternal":false,"tokens":2843,"sizeBytes":12450},{"name":"SKILL.md","path":"frontend/.claude/skills/form-refactorer/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/form-refactorer/SKILL.md","title":"form-refactorer","category":"anthropic-skill","format":"markdown","content":"---\nname: form-refactorer\ndescription: Refactor legacy forms to use modern Redpanda UI Registry Field components with react-hook-form and Zod validation. Use when user requests: (1) Form refactoring or modernization, (2) Updating forms to use Field components, (3) Migrating from legacy form patterns, (4) Implementing forms with react-hook-form and Zod validation.\n---\n\n# Form Refactorer\n\nRefactor legacy forms to modern Redpanda UI Registry Field components with react-hook-form and Zod validation.\n\n## Core Requirements\n\n### MUST USE (Modern Pattern)\n\n- Field component family from `components/redpanda-ui/components/field`\n- react-hook-form for form state management\n- Zod for validation schemas via `zodResolver`\n\n### FORBIDDEN (Legacy Patterns)\n\nSee [no-legacy](../code-standards/rules/no-legacy.md) for prohibited patterns. Also never import `Form` from `components/redpanda-ui/components/form`.\n\n## Component Hierarchy\n\n```\nFieldSet              - Semantic fieldset container for related fields\n├─ FieldLegend        - Title for the fieldset\n├─ FieldDescription   - Description for entire fieldset\n└─ FieldGroup         - Layout wrapper that stacks fields\n   ├─ Field           - Individual field wrapper\n   │  ├─ FieldLabel       - Label for input (use htmlFor)\n   │  ├─ Input/Select/... - Form control\n   │  ├─ FieldDescription - Helper text\n   │  └─ FieldError       - Error message\n   ├─ FieldSeparator  - Visual divider between fields\n   └─ Field           - Another field\n```\n\n## Key Patterns\n\n**Vertical field:**\n```tsx\n<Field data-invalid={!!errors.fieldName}>\n  <FieldLabel htmlFor=\"fieldName\">Label</FieldLabel>\n  <Input id=\"fieldName\" {...register(\"fieldName\")} aria-invalid={!!errors.fieldName} />\n  {errors.fieldName && <FieldError>{errors.fieldName.message}</FieldError>}\n</Field>\n```\n\n**Horizontal field (switch/checkbox):**\n```tsx\n<Field orientation=\"horizontal\">\n  <Switch id=\"toggle\" {...register(\"toggle\")} />\n  <FieldContent>\n    <FieldLabel htmlFor=\"toggle\">Enable Feature</FieldLabel>\n    <FieldDescription>Description</FieldDescription>\n  </FieldContent>\n</Field>\n```\n\n## Checklist\n\nBefore completing refactoring, verify:\n\n- [ ] All legacy form imports removed (the legacy `Form` component)\n- [ ] Field components used for all fields\n- [ ] FieldLabel has `htmlFor` matching Input `id`\n- [ ] Errors show `data-invalid` on Field and `aria-invalid` on Input\n- [ ] Yup replaced with Zod + zodResolver\n- [ ] FieldSet + FieldLegend used for semantic grouping\n- [ ] FieldError conditionally rendered: `{errors.field && <FieldError>...`\n- [ ] Form submission handled via react-hook-form `handleSubmit`\n\n## References\n\n- [Common Patterns](references/common-patterns.md) — Dynamic fields, field arrays, multi-section forms, pitfalls, Zod patterns\n- [Field Component Docs](https://redpanda-ui-registry.netlify.app/docs/field)\n","isInternal":false,"tokens":677,"sizeBytes":2896},{"name":"advanced-event-handler-refs.md","path":"frontend/.claude/skills/react-best-practices/rules/advanced-event-handler-refs.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/advanced-event-handler-refs.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Store Event Handlers in Refs\nimpact: LOW\nimpactDescription: stable subscriptions\ntags: advanced, hooks, refs, event-handlers, optimization\n---\n\n## Store Event Handlers in Refs\n\nStore callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.\n\n**Incorrect (re-subscribes on every render):**\n\n```tsx\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  useEffect(() => {\n    window.addEventListener(event, handler)\n    return () => window.removeEventListener(event, handler)\n  }, [event, handler])\n}\n```\n\n**Correct (stable subscription):**\n\n```tsx\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  const handlerRef = useRef(handler)\n  useEffect(() => {\n    handlerRef.current = handler\n  }, [handler])\n\n  useEffect(() => {\n    const listener = (e) => handlerRef.current(e)\n    window.addEventListener(event, listener)\n    return () => window.removeEventListener(event, listener)\n  }, [event])\n}\n```\n\n**Alternative: use `useEffectEvent` if you're on latest React:**\n\n```tsx\nimport { useEffectEvent } from 'react'\n\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  const onEvent = useEffectEvent(handler)\n\n  useEffect(() => {\n    window.addEventListener(event, onEvent)\n    return () => window.removeEventListener(event, onEvent)\n  }, [event])\n}\n```\n\n`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.\n","frontmatter":{"title":"Store Event Handlers in Refs","impact":"LOW","impactDescription":"stable subscriptions","tags":"advanced, hooks, refs, event-handlers, optimization"},"isInternal":false,"tokens":337,"sizeBytes":1483},{"name":"advanced-use-latest.md","path":"frontend/.claude/skills/react-best-practices/rules/advanced-use-latest.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/advanced-use-latest.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: useLatest for Stable Callback Refs\nimpact: LOW\nimpactDescription: prevents effect re-runs\ntags: advanced, hooks, useLatest, refs, optimization\n---\n\n## useLatest for Stable Callback Refs\n\nAccess latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.\n\n**Implementation:**\n\n```typescript\nfunction useLatest<T>(value: T) {\n  const ref = useRef(value)\n  useLayoutEffect(() => {\n    ref.current = value\n  }, [value])\n  return ref\n}\n```\n\n**Incorrect (effect re-runs on every callback change):**\n\n```tsx\nfunction SearchInput({ onSearch }: { onSearch: (q: string) => void }) {\n  const [query, setQuery] = useState('')\n\n  useEffect(() => {\n    const timeout = setTimeout(() => onSearch(query), 300)\n    return () => clearTimeout(timeout)\n  }, [query, onSearch])\n}\n```\n\n**Correct (stable effect, fresh callback):**\n\n```tsx\nfunction SearchInput({ onSearch }: { onSearch: (q: string) => void }) {\n  const [query, setQuery] = useState('')\n  const onSearchRef = useLatest(onSearch)\n\n  useEffect(() => {\n    const timeout = setTimeout(() => onSearchRef.current(query), 300)\n    return () => clearTimeout(timeout)\n  }, [query])\n}\n```\n","frontmatter":{"title":"useLatest for Stable Callback Refs","impact":"LOW","impactDescription":"prevents effect re-runs","tags":"advanced, hooks, useLatest, refs, optimization"},"isInternal":false,"tokens":295,"sizeBytes":1197},{"name":"async-defer-await.md","path":"frontend/.claude/skills/react-best-practices/rules/async-defer-await.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/async-defer-await.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Defer Await Until Needed\nimpact: HIGH\nimpactDescription: avoids blocking unused code paths\ntags: async, await, conditional, optimization\n---\n\n## Defer Await Until Needed\n\nMove `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.\n\n**Incorrect (blocks both branches):**\n\n```typescript\nasync function handleRequest(userId: string, skipProcessing: boolean) {\n  const userData = await fetchUserData(userId)\n  \n  if (skipProcessing) {\n    // Returns immediately but still waited for userData\n    return { skipped: true }\n  }\n  \n  // Only this branch uses userData\n  return processUserData(userData)\n}\n```\n\n**Correct (only blocks when needed):**\n\n```typescript\nasync function handleRequest(userId: string, skipProcessing: boolean) {\n  if (skipProcessing) {\n    // Returns immediately without waiting\n    return { skipped: true }\n  }\n  \n  // Fetch only when needed\n  const userData = await fetchUserData(userId)\n  return processUserData(userData)\n}\n```\n\n**Another example (early return optimization):**\n\n```typescript\n// Incorrect: always fetches permissions\nasync function updateResource(resourceId: string, userId: string) {\n  const permissions = await fetchPermissions(userId)\n  const resource = await getResource(resourceId)\n  \n  if (!resource) {\n    return { error: 'Not found' }\n  }\n  \n  if (!permissions.canEdit) {\n    return { error: 'Forbidden' }\n  }\n  \n  return await updateResourceData(resource, permissions)\n}\n\n// Correct: fetches only when needed\nasync function updateResource(resourceId: string, userId: string) {\n  const resource = await getResource(resourceId)\n  \n  if (!resource) {\n    return { error: 'Not found' }\n  }\n  \n  const permissions = await fetchPermissions(userId)\n  \n  if (!permissions.canEdit) {\n    return { error: 'Forbidden' }\n  }\n  \n  return await updateResourceData(resource, permissions)\n}\n```\n\nThis optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.\n","frontmatter":{"title":"Defer Await Until Needed","impact":"HIGH","impactDescription":"avoids blocking unused code paths","tags":"async, await, conditional, optimization"},"isInternal":false,"tokens":437,"sizeBytes":2028},{"name":"async-dependencies.md","path":"frontend/.claude/skills/react-best-practices/rules/async-dependencies.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/async-dependencies.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Dependency-Based Parallelization\nimpact: CRITICAL\nimpactDescription: 2-10× improvement\ntags: async, parallelization, dependencies, better-all\n---\n\n## Dependency-Based Parallelization\n\nFor operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.\n\n**Incorrect (profile waits for config unnecessarily):**\n\n```typescript\nconst [user, config] = await Promise.all([\n  fetchUser(),\n  fetchConfig()\n])\nconst profile = await fetchProfile(user.id)\n```\n\n**Correct (config and profile run in parallel):**\n\n```typescript\nimport { all } from 'better-all'\n\nconst { user, config, profile } = await all({\n  async user() { return fetchUser() },\n  async config() { return fetchConfig() },\n  async profile() {\n    return fetchProfile((await this.$.user).id)\n  }\n})\n```\n\nReference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)\n","frontmatter":{"title":"Dependency-Based Parallelization","impact":"CRITICAL","impactDescription":"2-10× improvement","tags":"async, parallelization, dependencies, better-all"},"isInternal":false,"tokens":213,"sizeBytes":942},{"name":"async-suspense-boundaries.md","path":"frontend/.claude/skills/react-best-practices/rules/async-suspense-boundaries.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/async-suspense-boundaries.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Strategic Suspense Boundaries\nimpact: HIGH\nimpactDescription: faster initial paint\ntags: async, suspense, streaming, layout-shift\n---\n\n## Strategic Suspense Boundaries\n\nInstead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.\n\n**Incorrect (wrapper blocked by data fetching):**\n\n```tsx\nasync function Page() {\n  const data = await fetchData() // Blocks entire page\n  \n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <div>\n        <DataDisplay data={data} />\n      </div>\n      <div>Footer</div>\n    </div>\n  )\n}\n```\n\nThe entire layout waits for data even though only the middle section needs it.\n\n**Correct (wrapper shows immediately, data streams in):**\n\n```tsx\nfunction Page() {\n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <div>\n        <Suspense fallback={<Skeleton />}>\n          <DataDisplay />\n        </Suspense>\n      </div>\n      <div>Footer</div>\n    </div>\n  )\n}\n\nasync function DataDisplay() {\n  const data = await fetchData() // Only blocks this component\n  return <div>{data.content}</div>\n}\n```\n\nSidebar, Header, and Footer render immediately. Only DataDisplay waits for data.\n\n**Alternative (share promise across components):**\n\n```tsx\nfunction Page() {\n  // Start fetch immediately, but don't await\n  const dataPromise = fetchData()\n  \n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <Suspense fallback={<Skeleton />}>\n        <DataDisplay dataPromise={dataPromise} />\n        <DataSummary dataPromise={dataPromise} />\n      </Suspense>\n      <div>Footer</div>\n    </div>\n  )\n}\n\nfunction DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {\n  const data = use(dataPromise) // Unwraps the promise\n  return <div>{data.content}</div>\n}\n\nfunction DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {\n  const data = use(dataPromise) // Reuses the same promise\n  return <div>{data.summary}</div>\n}\n```\n\nBoth components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.\n\n**When NOT to use this pattern:**\n\n- Critical data needed for layout decisions (affects positioning)\n- SEO-critical content above the fold\n- Small, fast queries where suspense overhead isn't worth it\n- When you want to avoid layout shift (loading → content jump)\n\n**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.\n","frontmatter":{"title":"Strategic Suspense Boundaries","impact":"HIGH","impactDescription":"faster initial paint","tags":"async, suspense, streaming, layout-shift"},"isInternal":false,"tokens":591,"sizeBytes":2510},{"name":"bundle-barrel-imports.md","path":"frontend/.claude/skills/react-best-practices/rules/bundle-barrel-imports.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/bundle-barrel-imports.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Avoid Barrel File Imports\nimpact: CRITICAL\nimpactDescription: 200-800ms import cost, slow builds\ntags: bundle, imports, tree-shaking, barrel-files, performance\n---\n\n## Avoid Barrel File Imports\n\nImport directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).\n\nPopular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.\n\n**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.\n\n**Incorrect (imports entire library):**\n\n```tsx\nimport { Check, X, Menu } from 'lucide-react'\n// Loads 1,583 modules, takes ~2.8s extra in dev\n// Runtime cost: 200-800ms on every cold start\n\nimport { Button, TextField } from '@mui/material'\n// Loads 2,225 modules, takes ~4.2s extra in dev\n```\n\n**Correct (imports only what you need):**\n\n```tsx\nimport Check from 'lucide-react/dist/esm/icons/check'\nimport X from 'lucide-react/dist/esm/icons/x'\nimport Menu from 'lucide-react/dist/esm/icons/menu'\n// Loads only 3 modules (~2KB vs ~1MB)\n\nimport Button from '@mui/material/Button'\nimport TextField from '@mui/material/TextField'\n// Loads only what you use\n```\n\nDirect imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.\n\nLibraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.\n\nReference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)\n","frontmatter":{"title":"Avoid Barrel File Imports","impact":"CRITICAL","impactDescription":"200-800ms import cost, slow builds","tags":"bundle, imports, tree-shaking, barrel-files, performance"},"isInternal":false,"tokens":499,"sizeBytes":1993},{"name":"bundle-code-splitting.md","path":"frontend/.claude/skills/react-best-practices/rules/bundle-code-splitting.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/bundle-code-splitting.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Code Splitting and Lazy Loading\nimpact: CRITICAL\nimpactDescription: directly affects TTI and LCP\ntags: bundle, dynamic-import, code-splitting, react-lazy, conditional-loading\n---\n\n## Code Splitting and Lazy Loading\n\nUse `React.lazy` with `Suspense` to lazy-load large components not needed on initial render. Load large data modules conditionally. Defer non-critical third-party libraries.\n\n### Dynamic Imports for Heavy Components\n\n**Incorrect (Monaco bundles with main chunk ~300KB):**\n\n```tsx\nimport { MonacoEditor } from './monaco-editor'\n```\n\n**Correct (Monaco loads on demand):**\n\n```tsx\nimport { lazy, Suspense } from 'react'\n\nconst MonacoEditor = lazy(() =>\n  import('./monaco-editor').then(m => ({ default: m.MonacoEditor }))\n)\n\nfunction CodePanel({ code }: { code: string }) {\n  return (\n    <Suspense fallback={<div>Loading editor...</div>}>\n      <MonacoEditor value={code} />\n    </Suspense>\n  )\n}\n```\n\n### Conditional Module Loading\n\nLoad large data or modules only when a feature is activated:\n\n```tsx\nfunction AnimationPlayer({ enabled, setEnabled }: Props) {\n  const [frames, setFrames] = useState<Frame[] | null>(null)\n\n  useEffect(() => {\n    if (enabled && !frames) {\n      import('./animation-frames.js')\n        .then(mod => setFrames(mod.frames))\n        .catch(() => setEnabled(false))\n    }\n  }, [enabled, frames, setEnabled])\n\n  if (!frames) return <Skeleton />\n  return <Canvas frames={frames} />\n}\n```\n\n### Defer Non-Critical Third-Party Libraries\n\nAnalytics, logging, and error tracking don't block user interaction:\n\n```tsx\nconst Analytics = lazy(() =>\n  import('@some-vendor/analytics').then(m => ({ default: m.Analytics }))\n)\n\nexport function App({ children }) {\n  return (\n    <div>\n      {children}\n      <Suspense fallback={null}>\n        <Analytics />\n      </Suspense>\n    </div>\n  )\n}\n```\n","frontmatter":{"title":"Code Splitting and Lazy Loading","impact":"CRITICAL","impactDescription":"directly affects TTI and LCP","tags":"bundle, dynamic-import, code-splitting, react-lazy, conditional-loading"},"isInternal":false,"tokens":446,"sizeBytes":1838},{"name":"bundle-preload.md","path":"frontend/.claude/skills/react-best-practices/rules/bundle-preload.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/bundle-preload.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Preload Based on User Intent\nimpact: MEDIUM\nimpactDescription: reduces perceived latency\ntags: bundle, preload, user-intent, hover\n---\n\n## Preload Based on User Intent\n\nPreload heavy bundles before they're needed to reduce perceived latency.\n\n**Example (preload on hover/focus):**\n\n```tsx\nfunction EditorButton({ onClick }: { onClick: () => void }) {\n  const preload = () => {\n    if (typeof window !== 'undefined') {\n      void import('./monaco-editor')\n    }\n  }\n\n  return (\n    <button\n      onMouseEnter={preload}\n      onFocus={preload}\n      onClick={onClick}\n    >\n      Open Editor\n    </button>\n  )\n}\n```\n\n**Example (preload when feature flag is enabled):**\n\n```tsx\nfunction FlagsProvider({ children, flags }: Props) {\n  useEffect(() => {\n    if (flags.editorEnabled && typeof window !== 'undefined') {\n      void import('./monaco-editor').then(mod => mod.init())\n    }\n  }, [flags.editorEnabled])\n\n  return <FlagsContext.Provider value={flags}>\n    {children}\n  </FlagsContext.Provider>\n}\n```\n\nThe `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.\n","frontmatter":{"title":"Preload Based on User Intent","impact":"MEDIUM","impactDescription":"reduces perceived latency","tags":"bundle, preload, user-intent, hover"},"isInternal":false,"tokens":267,"sizeBytes":1149},{"name":"client-event-listeners.md","path":"frontend/.claude/skills/react-best-practices/rules/client-event-listeners.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/client-event-listeners.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Deduplicate Global Event Listeners\nimpact: LOW\nimpactDescription: single listener for N components\ntags: client, swr, event-listeners, subscription\n---\n\n## Deduplicate Global Event Listeners\n\nUse `useSWRSubscription()` to share global event listeners across component instances.\n\n**Incorrect (N instances = N listeners):**\n\n```tsx\nfunction useKeyboardShortcut(key: string, callback: () => void) {\n  useEffect(() => {\n    const handler = (e: KeyboardEvent) => {\n      if (e.metaKey && e.key === key) {\n        callback()\n      }\n    }\n    window.addEventListener('keydown', handler)\n    return () => window.removeEventListener('keydown', handler)\n  }, [key, callback])\n}\n```\n\nWhen using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.\n\n**Correct (N instances = 1 listener):**\n\n```tsx\nimport useSWRSubscription from 'swr/subscription'\n\n// Module-level Map to track callbacks per key\nconst keyCallbacks = new Map<string, Set<() => void>>()\n\nfunction useKeyboardShortcut(key: string, callback: () => void) {\n  // Register this callback in the Map\n  useEffect(() => {\n    if (!keyCallbacks.has(key)) {\n      keyCallbacks.set(key, new Set())\n    }\n    keyCallbacks.get(key)!.add(callback)\n\n    return () => {\n      const set = keyCallbacks.get(key)\n      if (set) {\n        set.delete(callback)\n        if (set.size === 0) {\n          keyCallbacks.delete(key)\n        }\n      }\n    }\n  }, [key, callback])\n\n  useSWRSubscription('global-keydown', () => {\n    const handler = (e: KeyboardEvent) => {\n      if (e.metaKey && keyCallbacks.has(e.key)) {\n        keyCallbacks.get(e.key)!.forEach(cb => cb())\n      }\n    }\n    window.addEventListener('keydown', handler)\n    return () => window.removeEventListener('keydown', handler)\n  })\n}\n\nfunction Profile() {\n  // Multiple shortcuts will share the same listener\n  useKeyboardShortcut('p', () => { /* ... */ }) \n  useKeyboardShortcut('k', () => { /* ... */ })\n  // ...\n}\n```\n","frontmatter":{"title":"Deduplicate Global Event Listeners","impact":"LOW","impactDescription":"single listener for N components","tags":"client, swr, event-listeners, subscription"},"isInternal":false,"tokens":466,"sizeBytes":1969},{"name":"client-passive-event-listeners.md","path":"frontend/.claude/skills/react-best-practices/rules/client-passive-event-listeners.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/client-passive-event-listeners.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Use Passive Event Listeners for Scrolling Performance\nimpact: MEDIUM\nimpactDescription: eliminates scroll delay caused by event listeners\ntags: client, event-listeners, scrolling, performance, touch, wheel\n---\n\n## Use Passive Event Listeners for Scrolling Performance\n\nAdd `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay.\n\n**Incorrect:**\n\n```typescript\nuseEffect(() => {\n  const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)\n  const handleWheel = (e: WheelEvent) => console.log(e.deltaY)\n  \n  document.addEventListener('touchstart', handleTouch)\n  document.addEventListener('wheel', handleWheel)\n  \n  return () => {\n    document.removeEventListener('touchstart', handleTouch)\n    document.removeEventListener('wheel', handleWheel)\n  }\n}, [])\n```\n\n**Correct:**\n\n```typescript\nuseEffect(() => {\n  const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)\n  const handleWheel = (e: WheelEvent) => console.log(e.deltaY)\n  \n  document.addEventListener('touchstart', handleTouch, { passive: true })\n  document.addEventListener('wheel', handleWheel, { passive: true })\n  \n  return () => {\n    document.removeEventListener('touchstart', handleTouch)\n    document.removeEventListener('wheel', handleWheel)\n  }\n}, [])\n```\n\n**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.\n\n**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.\n","frontmatter":{"title":"Use Passive Event Listeners for Scrolling Performance","impact":"MEDIUM","impactDescription":"eliminates scroll delay caused by event listeners","tags":"client, event-listeners, scrolling, performance, touch, wheel"},"isInternal":false,"tokens":361,"sizeBytes":1644},{"name":"js-batch-dom-css.md","path":"frontend/.claude/skills/react-best-practices/rules/js-batch-dom-css.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/js-batch-dom-css.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Batch DOM CSS Changes\nimpact: MEDIUM\nimpactDescription: reduces reflows/repaints\ntags: javascript, dom, css, performance, reflow\n---\n\n## Batch DOM CSS Changes\n\nAvoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.\n\n**Incorrect (interleaved reads and writes force reflows):**\n\n```typescript\nfunction updateElementStyles(element: HTMLElement) {\n  element.style.width = '100px'\n  const width = element.offsetWidth  // Forces reflow\n  element.style.height = '200px'\n  const height = element.offsetHeight  // Forces another reflow\n}\n```\n\n**Correct (batch writes, then read once):**\n\n```typescript\nfunction updateElementStyles(element: HTMLElement) {\n  // Batch all writes together\n  element.style.width = '100px'\n  element.style.height = '200px'\n  element.style.backgroundColor = 'blue'\n  element.style.border = '1px solid black'\n  \n  // Read after all writes are done (single reflow)\n  const { width, height } = element.getBoundingClientRect()\n}\n```\n\n**Better: use CSS classes**\n\n```css\n.highlighted-box {\n  width: 100px;\n  height: 200px;\n  background-color: blue;\n  border: 1px solid black;\n}\n```\n\n```typescript\nfunction updateElementStyles(element: HTMLElement) {\n  element.classList.add('highlighted-box')\n\n  const { width, height } = element.getBoundingClientRect()\n}\n```\n\nPrefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.","frontmatter":{"title":"Batch DOM CSS Changes","impact":"MEDIUM","impactDescription":"reduces reflows/repaints","tags":"javascript, dom, css, performance, reflow"},"isInternal":false,"tokens":370,"sizeBytes":1631},{"name":"js-caching-patterns.md","path":"frontend/.claude/skills/react-best-practices/rules/js-caching-patterns.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/js-caching-patterns.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Client-Side Caching Patterns\nimpact: MEDIUM\nimpactDescription: reduces redundant computation and expensive I/O\ntags: javascript, cache, memoization, localStorage, storage, performance\n---\n\n## Client-Side Caching Patterns\n\n### Cache Repeated Function Calls\n\nUse a module-level Map to cache function results when the same function is called repeatedly with the same inputs.\n\n```typescript\nconst slugifyCache = new Map<string, string>()\n\nfunction cachedSlugify(text: string): string {\n  if (slugifyCache.has(text)) return slugifyCache.get(text)!\n  const result = slugify(text)\n  slugifyCache.set(text, result)\n  return result\n}\n```\n\nUse a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.\n\n### Cache Storage API Calls\n\n`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory:\n\n```typescript\nconst storageCache = new Map<string, string | null>()\n\nfunction getLocalStorage(key: string) {\n  if (!storageCache.has(key)) {\n    storageCache.set(key, localStorage.getItem(key))\n  }\n  return storageCache.get(key)\n}\n\nfunction setLocalStorage(key: string, value: string) {\n  localStorage.setItem(key, value)\n  storageCache.set(key, value)\n}\n```\n\nInvalidate on external changes:\n\n```typescript\nwindow.addEventListener('storage', (e) => {\n  if (e.key) storageCache.delete(e.key)\n})\n```\n\n### Version localStorage Data\n\nAdd version prefix to keys and store only needed fields:\n\n```typescript\nconst VERSION = 'v2'\n\nfunction saveConfig(config: { theme: string; language: string }) {\n  try {\n    localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))\n  } catch {\n    // Throws in incognito, quota exceeded, or disabled\n  }\n}\n```\n\nAlways wrap `getItem()`/`setItem()` in try-catch — they throw in incognito/private browsing (Safari, Firefox).\n\n## Reference\n\n- [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)\n","frontmatter":{"title":"Client-Side Caching Patterns","impact":"MEDIUM","impactDescription":"reduces redundant computation and expensive I/O","tags":"javascript, cache, memoization, localStorage, storage, performance"},"isInternal":false,"tokens":450,"sizeBytes":1986},{"name":"js-index-maps.md","path":"frontend/.claude/skills/react-best-practices/rules/js-index-maps.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/js-index-maps.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Build Index Maps for Repeated Lookups\nimpact: LOW-MEDIUM\nimpactDescription: 1M ops to 2K ops\ntags: javascript, map, indexing, optimization, performance\n---\n\n## Build Index Maps for Repeated Lookups\n\nMultiple `.find()` calls by the same key should use a Map.\n\n**Incorrect (O(n) per lookup):**\n\n```typescript\nfunction processOrders(orders: Order[], users: User[]) {\n  return orders.map(order => ({\n    ...order,\n    user: users.find(u => u.id === order.userId)\n  }))\n}\n```\n\n**Correct (O(1) per lookup):**\n\n```typescript\nfunction processOrders(orders: Order[], users: User[]) {\n  const userById = new Map(users.map(u => [u.id, u]))\n\n  return orders.map(order => ({\n    ...order,\n    user: userById.get(order.userId)\n  }))\n}\n```\n\nBuild map once (O(n)), then all lookups are O(1).\nFor 1000 orders × 1000 users: 1M ops → 2K ops.\n","frontmatter":{"title":"Build Index Maps for Repeated Lookups","impact":"LOW-MEDIUM","impactDescription":"1M ops to 2K ops","tags":"javascript, map, indexing, optimization, performance"},"isInternal":false,"tokens":229,"sizeBytes":837},{"name":"js-length-check-first.md","path":"frontend/.claude/skills/react-best-practices/rules/js-length-check-first.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/js-length-check-first.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Early Length Check for Array Comparisons\nimpact: MEDIUM-HIGH\nimpactDescription: avoids expensive operations when lengths differ\ntags: javascript, arrays, performance, optimization, comparison\n---\n\n## Early Length Check for Array Comparisons\n\nWhen comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.\n\nIn real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).\n\n**Incorrect (always runs expensive comparison):**\n\n```typescript\nfunction hasChanges(current: string[], original: string[]) {\n  // Always sorts and joins, even when lengths differ\n  return current.sort().join() !== original.sort().join()\n}\n```\n\nTwo O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.\n\n**Correct (O(1) length check first):**\n\n```typescript\nfunction hasChanges(current: string[], original: string[]) {\n  // Early return if lengths differ\n  if (current.length !== original.length) {\n    return true\n  }\n  // Only sort when lengths match\n  const currentSorted = current.toSorted()\n  const originalSorted = original.toSorted()\n  for (let i = 0; i < currentSorted.length; i++) {\n    if (currentSorted[i] !== originalSorted[i]) {\n      return true\n    }\n  }\n  return false\n}\n```\n\nThis new approach is more efficient because:\n- It avoids the overhead of sorting and joining the arrays when lengths differ\n- It avoids consuming memory for the joined strings (especially important for large arrays)\n- It avoids mutating the original arrays\n- It returns early when a difference is found\n","frontmatter":{"title":"Early Length Check for Array Comparisons","impact":"MEDIUM-HIGH","impactDescription":"avoids expensive operations when lengths differ","tags":"javascript, arrays, performance, optimization, comparison"},"isInternal":false,"tokens":373,"sizeBytes":1747},{"name":"js-tosorted-immutable.md","path":"frontend/.claude/skills/react-best-practices/rules/js-tosorted-immutable.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/js-tosorted-immutable.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Use toSorted() Instead of sort() for Immutability\nimpact: MEDIUM-HIGH\nimpactDescription: prevents mutation bugs in React state\ntags: javascript, arrays, immutability, react, state, mutation\n---\n\n## Use toSorted() Instead of sort() for Immutability\n\n`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.\n\n**Incorrect (mutates original array):**\n\n```typescript\nfunction UserList({ users }: { users: User[] }) {\n  // Mutates the users prop array!\n  const sorted = useMemo(\n    () => users.sort((a, b) => a.name.localeCompare(b.name)),\n    [users]\n  )\n  return <div>{sorted.map(renderUser)}</div>\n}\n```\n\n**Correct (creates new array):**\n\n```typescript\nfunction UserList({ users }: { users: User[] }) {\n  // Creates new sorted array, original unchanged\n  const sorted = useMemo(\n    () => users.toSorted((a, b) => a.name.localeCompare(b.name)),\n    [users]\n  )\n  return <div>{sorted.map(renderUser)}</div>\n}\n```\n\n**Why this matters in React:**\n\n1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only\n2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior\n\n**Browser support (fallback for older browsers):**\n\n`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:\n\n```typescript\n// Fallback for older browsers\nconst sorted = [...items].sort((a, b) => a.value - b.value)\n```\n\n**Other immutable array methods:**\n\n- `.toSorted()` - immutable sort\n- `.toReversed()` - immutable reverse\n- `.toSpliced()` - immutable splice\n- `.with()` - immutable element replacement\n","frontmatter":{"title":"Use toSorted() Instead of sort() for Immutability","impact":"MEDIUM-HIGH","impactDescription":"prevents mutation bugs in React state","tags":"javascript, arrays, immutability, react, state, mutation"},"isInternal":false,"tokens":436,"sizeBytes":1782},{"name":"rendering-activity.md","path":"frontend/.claude/skills/react-best-practices/rules/rendering-activity.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/rendering-activity.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Use Activity Component for Show/Hide\nimpact: MEDIUM\nimpactDescription: preserves state/DOM\ntags: rendering, activity, visibility, state-preservation\n---\n\n## Use Activity Component for Show/Hide\n\nUse React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.\n\n**Usage:**\n\n```tsx\nimport { Activity } from 'react'\n\nfunction Dropdown({ isOpen }: Props) {\n  return (\n    <Activity mode={isOpen ? 'visible' : 'hidden'}>\n      <ExpensiveMenu />\n    </Activity>\n  )\n}\n```\n\nAvoids expensive re-renders and state loss.\n","frontmatter":{"title":"Use Activity Component for Show/Hide","impact":"MEDIUM","impactDescription":"preserves state/DOM","tags":"rendering, activity, visibility, state-preservation"},"isInternal":false,"tokens":131,"sizeBytes":564},{"name":"rendering-animate-svg-wrapper.md","path":"frontend/.claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Animate SVG Wrapper Instead of SVG Element\nimpact: LOW\nimpactDescription: enables hardware acceleration\ntags: rendering, svg, css, animation, performance\n---\n\n## Animate SVG Wrapper Instead of SVG Element\n\nMany browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.\n\n**Incorrect (animating SVG directly - no hardware acceleration):**\n\n```tsx\nfunction LoadingSpinner() {\n  return (\n    <svg \n      className=\"animate-spin\"\n      width=\"24\" \n      height=\"24\" \n      viewBox=\"0 0 24 24\"\n    >\n      <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" />\n    </svg>\n  )\n}\n```\n\n**Correct (animating wrapper div - hardware accelerated):**\n\n```tsx\nfunction LoadingSpinner() {\n  return (\n    <div className=\"animate-spin\">\n      <svg \n        width=\"24\" \n        height=\"24\" \n        viewBox=\"0 0 24 24\"\n      >\n        <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" />\n      </svg>\n    </div>\n  )\n}\n```\n\nThis applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.\n","frontmatter":{"title":"Animate SVG Wrapper Instead of SVG Element","impact":"LOW","impactDescription":"enables hardware acceleration","tags":"rendering, svg, css, animation, performance"},"isInternal":false,"tokens":291,"sizeBytes":1185},{"name":"rendering-conditional-render.md","path":"frontend/.claude/skills/react-best-practices/rules/rendering-conditional-render.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/rendering-conditional-render.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Use Explicit Conditional Rendering\nimpact: LOW\nimpactDescription: prevents rendering 0 or NaN\ntags: rendering, conditional, jsx, falsy-values\n---\n\n## Use Explicit Conditional Rendering\n\nUse explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.\n\n**Incorrect (renders \"0\" when count is 0):**\n\n```tsx\nfunction Badge({ count }: { count: number }) {\n  return (\n    <div>\n      {count && <span className=\"badge\">{count}</span>}\n    </div>\n  )\n}\n\n// When count = 0, renders: <div>0</div>\n// When count = 5, renders: <div><span class=\"badge\">5</span></div>\n```\n\n**Correct (renders nothing when count is 0):**\n\n```tsx\nfunction Badge({ count }: { count: number }) {\n  return (\n    <div>\n      {count > 0 ? <span className=\"badge\">{count}</span> : null}\n    </div>\n  )\n}\n\n// When count = 0, renders: <div></div>\n// When count = 5, renders: <div><span class=\"badge\">5</span></div>\n```\n","frontmatter":{"title":"Use Explicit Conditional Rendering","impact":"LOW","impactDescription":"prevents rendering 0 or NaN","tags":"rendering, conditional, jsx, falsy-values"},"isInternal":false,"tokens":273,"sizeBytes":980},{"name":"rendering-content-visibility.md","path":"frontend/.claude/skills/react-best-practices/rules/rendering-content-visibility.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/rendering-content-visibility.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: CSS content-visibility for Long Lists\nimpact: HIGH\nimpactDescription: faster initial render\ntags: rendering, css, content-visibility, long-lists\n---\n\n## CSS content-visibility for Long Lists\n\nApply `content-visibility: auto` to defer off-screen rendering.\n\n**CSS:**\n\n```css\n.message-item {\n  content-visibility: auto;\n  contain-intrinsic-size: 0 80px;\n}\n```\n\n**Example:**\n\n```tsx\nfunction MessageList({ messages }: { messages: Message[] }) {\n  return (\n    <div className=\"overflow-y-auto h-screen\">\n      {messages.map(msg => (\n        <div key={msg.id} className=\"message-item\">\n          <Avatar user={msg.author} />\n          <div>{msg.content}</div>\n        </div>\n      ))}\n    </div>\n  )\n}\n```\n\nFor 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).\n","frontmatter":{"title":"CSS content-visibility for Long Lists","impact":"HIGH","impactDescription":"faster initial render","tags":"rendering, css, content-visibility, long-lists"},"isInternal":false,"tokens":203,"sizeBytes":815},{"name":"rendering-hoist-jsx.md","path":"frontend/.claude/skills/react-best-practices/rules/rendering-hoist-jsx.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/rendering-hoist-jsx.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Hoist Static JSX Elements\nimpact: LOW\nimpactDescription: avoids re-creation\ntags: rendering, jsx, static, optimization\n---\n\n## Hoist Static JSX Elements\n\nExtract static JSX outside components to avoid re-creation.\n\n**Incorrect (recreates element every render):**\n\n```tsx\nfunction LoadingSkeleton() {\n  return <div className=\"animate-pulse h-20 bg-surface-strong\" />\n}\n\nfunction Container() {\n  return (\n    <div>\n      {loading && <LoadingSkeleton />}\n    </div>\n  )\n}\n```\n\n**Correct (reuses same element):**\n\n```tsx\nconst loadingSkeleton = (\n  <div className=\"animate-pulse h-20 bg-surface-strong\" />\n)\n\nfunction Container() {\n  return (\n    <div>\n      {loading && loadingSkeleton}\n    </div>\n  )\n}\n```\n\nThis is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.\n\n**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.\n","frontmatter":{"title":"Hoist Static JSX Elements","impact":"LOW","impactDescription":"avoids re-creation","tags":"rendering, jsx, static, optimization"},"isInternal":false,"tokens":244,"sizeBytes":1051},{"name":"rendering-svg-precision.md","path":"frontend/.claude/skills/react-best-practices/rules/rendering-svg-precision.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/rendering-svg-precision.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Optimize SVG Precision\nimpact: LOW\nimpactDescription: reduces file size\ntags: rendering, svg, optimization, svgo\n---\n\n## Optimize SVG Precision\n\nReduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.\n\n**Incorrect (excessive precision):**\n\n```svg\n<path d=\"M 10.293847 20.847362 L 30.938472 40.192837\" />\n```\n\n**Correct (1 decimal place):**\n\n```svg\n<path d=\"M 10.3 20.8 L 30.9 40.2\" />\n```\n\n**Automate with SVGO:**\n\n```bash\nnpx svgo --precision=1 --multipass icon.svg\n```\n","frontmatter":{"title":"Optimize SVG Precision","impact":"LOW","impactDescription":"reduces file size","tags":"rendering, svg, optimization, svgo"},"isInternal":false,"tokens":167,"sizeBytes":588},{"name":"rerender-defer-reads.md","path":"frontend/.claude/skills/react-best-practices/rules/rerender-defer-reads.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/rerender-defer-reads.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Defer State Reads to Usage Point\nimpact: MEDIUM\nimpactDescription: avoids unnecessary subscriptions\ntags: rerender, URLSearchParams, localStorage, optimization\n---\n\n## Defer State Reads to Usage Point\n\nDon't subscribe to dynamic state (URL params, localStorage) if you only read it inside callbacks.\n\n**Incorrect (subscribes to URL param changes, re-renders on every change):**\n\n```tsx\nfunction ShareButton({ chatId }: { chatId: string }) {\n  // Reading URL params at render time creates a subscription\n  const ref = new URLSearchParams(window.location.search).get('ref')\n\n  const handleShare = () => {\n    shareChat(chatId, { ref })\n  }\n\n  return <button onClick={handleShare}>Share</button>\n}\n```\n\n**Correct (reads on demand inside callback, no subscription):**\n\n```tsx\nfunction ShareButton({ chatId }: { chatId: string }) {\n  const handleShare = () => {\n    const params = new URLSearchParams(window.location.search)\n    const ref = params.get('ref')\n    shareChat(chatId, { ref })\n  }\n\n  return <button onClick={handleShare}>Share</button>\n}\n```\n\nSame principle applies to localStorage, sessionStorage, and cookies — read inside callbacks rather than at render time.\n","frontmatter":{"title":"Defer State Reads to Usage Point","impact":"MEDIUM","impactDescription":"avoids unnecessary subscriptions","tags":"rerender, URLSearchParams, localStorage, optimization"},"isInternal":false,"tokens":269,"sizeBytes":1184},{"name":"rerender-dependencies.md","path":"frontend/.claude/skills/react-best-practices/rules/rerender-dependencies.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/rerender-dependencies.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Narrow Effect Dependencies\nimpact: LOW\nimpactDescription: minimizes effect re-runs\ntags: rerender, useEffect, dependencies, optimization\n---\n\n## Narrow Effect Dependencies\n\nSpecify primitive dependencies instead of objects to minimize effect re-runs.\n\n**Incorrect (re-runs on any user field change):**\n\n```tsx\nuseEffect(() => {\n  console.log(user.id)\n}, [user])\n```\n\n**Correct (re-runs only when id changes):**\n\n```tsx\nuseEffect(() => {\n  console.log(user.id)\n}, [user.id])\n```\n\n**For derived state, compute outside effect:**\n\n```tsx\n// Incorrect: runs on width=767, 766, 765...\nuseEffect(() => {\n  if (width < 768) {\n    enableMobileMode()\n  }\n}, [width])\n\n// Correct: runs only on boolean transition\nconst isMobile = width < 768\nuseEffect(() => {\n  if (isMobile) {\n    enableMobileMode()\n  }\n}, [isMobile])\n```\n","frontmatter":{"title":"Narrow Effect Dependencies","impact":"LOW","impactDescription":"minimizes effect re-runs","tags":"rerender, useEffect, dependencies, optimization"},"isInternal":false,"tokens":214,"sizeBytes":824},{"name":"rerender-derived-state.md","path":"frontend/.claude/skills/react-best-practices/rules/rerender-derived-state.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/rerender-derived-state.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Subscribe to Derived State\nimpact: MEDIUM\nimpactDescription: reduces re-render frequency\ntags: rerender, derived-state, media-query, optimization\n---\n\n## Subscribe to Derived State\n\nSubscribe to derived boolean state instead of continuous values to reduce re-render frequency.\n\n**Incorrect (re-renders on every pixel change):**\n\n```tsx\nfunction Sidebar() {\n  const width = useWindowWidth()  // updates continuously\n  const isMobile = width < 768\n  return <nav className={isMobile ? 'mobile' : 'desktop'} />\n}\n```\n\n**Correct (re-renders only when boolean changes):**\n\n```tsx\nfunction Sidebar() {\n  const isMobile = useMediaQuery('(max-width: 767px)')\n  return <nav className={isMobile ? 'mobile' : 'desktop'} />\n}\n```\n","frontmatter":{"title":"Subscribe to Derived State","impact":"MEDIUM","impactDescription":"reduces re-render frequency","tags":"rerender, derived-state, media-query, optimization"},"isInternal":false,"tokens":175,"sizeBytes":728},{"name":"rerender-functional-setstate.md","path":"frontend/.claude/skills/react-best-practices/rules/rerender-functional-setstate.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/rerender-functional-setstate.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Use Functional setState Updates\nimpact: MEDIUM\nimpactDescription: prevents stale closures and unnecessary callback recreations\ntags: react, hooks, useState, useCallback, callbacks, closures\n---\n\n## Use Functional setState Updates\n\nWhen updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.\n\n**Incorrect (requires state as dependency):**\n\n```tsx\nfunction TodoList() {\n  const [items, setItems] = useState(initialItems)\n  \n  // Callback must depend on items, recreated on every items change\n  const addItems = useCallback((newItems: Item[]) => {\n    setItems([...items, ...newItems])\n  }, [items])  // ❌ items dependency causes recreations\n  \n  // Risk of stale closure if dependency is forgotten\n  const removeItem = useCallback((id: string) => {\n    setItems(items.filter(item => item.id !== id))\n  }, [])  // ❌ Missing items dependency - will use stale items!\n  \n  return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />\n}\n```\n\nThe first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.\n\n**Correct (stable callbacks, no stale closures):**\n\n```tsx\nfunction TodoList() {\n  const [items, setItems] = useState(initialItems)\n  \n  // Stable callback, never recreated\n  const addItems = useCallback((newItems: Item[]) => {\n    setItems(curr => [...curr, ...newItems])\n  }, [])  // ✅ No dependencies needed\n  \n  // Always uses latest state, no stale closure risk\n  const removeItem = useCallback((id: string) => {\n    setItems(curr => curr.filter(item => item.id !== id))\n  }, [])  // ✅ Safe and stable\n  \n  return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />\n}\n```\n\n**Benefits:**\n\n1. **Stable callback references** - Callbacks don't need to be recreated when state changes\n2. **No stale closures** - Always operates on the latest state value\n3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks\n4. **Prevents bugs** - Eliminates the most common source of React closure bugs\n\n**When to use functional updates:**\n\n- Any setState that depends on the current state value\n- Inside useCallback/useMemo when state is needed\n- Event handlers that reference state\n- Async operations that update state\n\n**When direct updates are fine:**\n\n- Setting state to a static value: `setCount(0)`\n- Setting state from props/arguments only: `setName(newName)`\n- State doesn't depend on previous value\n\n**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.\n","frontmatter":{"title":"Use Functional setState Updates","impact":"MEDIUM","impactDescription":"prevents stale closures and unnecessary callback recreations","tags":"react, hooks, useState, useCallback, callbacks, closures"},"isInternal":false,"tokens":654,"sizeBytes":2968},{"name":"rerender-lazy-state-init.md","path":"frontend/.claude/skills/react-best-practices/rules/rerender-lazy-state-init.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/rerender-lazy-state-init.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Use Lazy State Initialization\nimpact: MEDIUM\nimpactDescription: wasted computation on every render\ntags: react, hooks, useState, performance, initialization\n---\n\n## Use Lazy State Initialization\n\nPass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.\n\n**Incorrect (runs on every render):**\n\n```tsx\nfunction FilteredList({ items }: { items: Item[] }) {\n  // buildSearchIndex() runs on EVERY render, even after initialization\n  const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))\n  const [query, setQuery] = useState('')\n  \n  // When query changes, buildSearchIndex runs again unnecessarily\n  return <SearchResults index={searchIndex} query={query} />\n}\n\nfunction UserProfile() {\n  // JSON.parse runs on every render\n  const [settings, setSettings] = useState(\n    JSON.parse(localStorage.getItem('settings') || '{}')\n  )\n  \n  return <SettingsForm settings={settings} onChange={setSettings} />\n}\n```\n\n**Correct (runs only once):**\n\n```tsx\nfunction FilteredList({ items }: { items: Item[] }) {\n  // buildSearchIndex() runs ONLY on initial render\n  const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))\n  const [query, setQuery] = useState('')\n  \n  return <SearchResults index={searchIndex} query={query} />\n}\n\nfunction UserProfile() {\n  // JSON.parse runs only on initial render\n  const [settings, setSettings] = useState(() => {\n    const stored = localStorage.getItem('settings')\n    return stored ? JSON.parse(stored) : {}\n  })\n  \n  return <SettingsForm settings={settings} onChange={setSettings} />\n}\n```\n\nUse lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.\n\nFor simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.\n","frontmatter":{"title":"Use Lazy State Initialization","impact":"MEDIUM","impactDescription":"wasted computation on every render","tags":"react, hooks, useState, performance, initialization"},"isInternal":false,"tokens":438,"sizeBytes":2016},{"name":"rerender-memo.md","path":"frontend/.claude/skills/react-best-practices/rules/rerender-memo.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/rerender-memo.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Extract to Memoized Components\nimpact: MEDIUM\nimpactDescription: enables early returns\ntags: rerender, memo, useMemo, optimization\n---\n\n## Extract to Memoized Components\n\nExtract expensive work into memoized components to enable early returns before computation.\n\n**Incorrect (computes avatar even when loading):**\n\n```tsx\nfunction Profile({ user, loading }: Props) {\n  const avatar = useMemo(() => {\n    const id = computeAvatarId(user)\n    return <Avatar id={id} />\n  }, [user])\n\n  if (loading) return <Skeleton />\n  return <div>{avatar}</div>\n}\n```\n\n**Correct (skips computation when loading):**\n\n```tsx\nconst UserAvatar = memo(function UserAvatar({ user }: { user: User }) {\n  const id = useMemo(() => computeAvatarId(user), [user])\n  return <Avatar id={id} />\n})\n\nfunction Profile({ user, loading }: Props) {\n  if (loading) return <Skeleton />\n  return (\n    <div>\n      <UserAvatar user={user} />\n    </div>\n  )\n}\n```\n\n**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.\n","frontmatter":{"title":"Extract to Memoized Components","impact":"MEDIUM","impactDescription":"enables early returns","tags":"rerender, memo, useMemo, optimization"},"isInternal":false,"tokens":278,"sizeBytes":1148},{"name":"rerender-simple-expression-in-memo.md","path":"frontend/.claude/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Freact-best-practices%2Frules%2Frerender-simple-expression-in-memo.md -->","frontmatter":{"title":"Do not wrap a simple expression with a primitive result type in useMemo","impact":"LOW-MEDIUM","impactDescription":"wasted computation on every render","tags":"rerender, useMemo, optimization"},"isInternal":false,"tokens":217,"sizeBytes":1018},{"name":"rerender-transitions.md","path":"frontend/.claude/skills/react-best-practices/rules/rerender-transitions.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/rerender-transitions.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Freact-best-practices%2Frules%2Frerender-transitions.md -->","frontmatter":{"title":"Use Transitions for Non-Urgent Updates","impact":"MEDIUM","impactDescription":"maintains UI responsiveness","tags":"rerender, transitions, startTransition, performance"},"isInternal":false,"tokens":241,"sizeBytes":1055},{"name":"use-effect.md","path":"frontend/.claude/skills/react-best-practices/rules/use-effect.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/rules/use-effect.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Freact-best-practices%2Frules%2Fuse-effect.md -->","isInternal":false,"tokens":1336,"sizeBytes":5565},{"name":"SKILL.md","path":"frontend/.claude/skills/react-best-practices/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/react-best-practices/SKILL.md","title":"react-best-practices","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Freact-best-practices%2FSKILL.md -->","frontmatter":{"name":"react-best-practices","description":"Client-side React performance optimization patterns."},"isInternal":false,"tokens":757,"sizeBytes":3113},{"name":"_sections.md","path":"frontend/.claude/skills/security-scan/rules/_sections.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/security-scan/rules/_sections.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fsecurity-scan%2Frules%2F_sections.md -->","isInternal":false,"tokens":43,"sizeBytes":191},{"name":"_template.md","path":"frontend/.claude/skills/security-scan/rules/_template.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/security-scan/rules/_template.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fsecurity-scan%2Frules%2F_template.md -->","frontmatter":{"title":["Rule Title"],"impact":["CRITICAL|HIGH|MEDIUM|LOW"],"impactDescription":[{"Optional":"quantified benefit"}],"tags":["comma","separated","tags"]},"isInternal":false,"tokens":109,"sizeBytes":432},{"name":"vuln-direct-deps.md","path":"frontend/.claude/skills/security-scan/rules/vuln-direct-deps.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/security-scan/rules/vuln-direct-deps.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fsecurity-scan%2Frules%2Fvuln-direct-deps.md -->","frontmatter":{"title":"Fix Direct Dependency Vulnerabilities","impact":"CRITICAL","impactDescription":"Direct vulnerabilities are exploitable and must be fixed immediately","tags":"security, npm, dependencies, vulnerabilities"},"isInternal":false,"tokens":398,"sizeBytes":1677},{"name":"vuln-transitive-deps.md","path":"frontend/.claude/skills/security-scan/rules/vuln-transitive-deps.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/security-scan/rules/vuln-transitive-deps.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fsecurity-scan%2Frules%2Fvuln-transitive-deps.md -->","frontmatter":{"title":"Fix Transitive Dependency Vulnerabilities","impact":"HIGH","impactDescription":"Transitive vulnerabilities can still be exploited through parent packages","tags":"security, npm, overrides, transitive, dependencies"},"isInternal":false,"tokens":460,"sizeBytes":1851},{"name":"SKILL.md","path":"frontend/.claude/skills/security-scan/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/security-scan/SKILL.md","title":"security-scan","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fsecurity-scan%2FSKILL.md -->","frontmatter":{"name":"security-scan","description":"Resolve npm dependency vulnerabilities detected by security scans."},"isInternal":false,"tokens":342,"sizeBytes":1404},{"name":"use-react-query-for-server.md","path":"frontend/.claude/skills/state-management/rules/use-react-query-for-server.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/state-management/rules/use-react-query-for-server.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fstate-management%2Frules%2Fuse-react-query-for-server.md -->","frontmatter":{"title":"Use React Query for Server State","impact":"HIGH","impactDescription":"Using client state for server data leads to stale data and cache inconsistencies","tags":"react-query, connect-query, server-state, caching"},"isInternal":false,"tokens":505,"sizeBytes":2161},{"name":"use-zustand-persist.md","path":"frontend/.claude/skills/state-management/rules/use-zustand-persist.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/state-management/rules/use-zustand-persist.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fstate-management%2Frules%2Fuse-zustand-persist.md -->","frontmatter":{"title":"Zustand Persistence Pattern","impact":"HIGH","impactDescription":"Proper persistence ensures state survives navigation and page reloads","tags":"zustand, persist, sessionStorage, middleware"},"isInternal":false,"tokens":482,"sizeBytes":1975},{"name":"use-zustand-selectors.md","path":"frontend/.claude/skills/state-management/rules/use-zustand-selectors.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/state-management/rules/use-zustand-selectors.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fstate-management%2Frules%2Fuse-zustand-selectors.md -->","frontmatter":{"title":"Use Zustand Selectors","impact":"CRITICAL","impactDescription":"Missing selectors cause unnecessary re-renders on every store update","tags":"zustand, selectors, performance, react"},"isInternal":false,"tokens":426,"sizeBytes":1840},{"name":"SKILL.md","path":"frontend/.claude/skills/state-management/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/state-management/SKILL.md","title":"state-management","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fstate-management%2FSKILL.md -->","frontmatter":{"name":"state-management","description":"Manage client and server state with Zustand stores and React Query patterns."},"isInternal":false,"tokens":276,"sizeBytes":1248},{"name":"build-configuration.md","path":"frontend/.claude/skills/tanstack-router-migration/references/build-configuration.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/tanstack-router-migration/references/build-configuration.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftanstack-router-migration%2Freferences%2Fbuild-configuration.md -->","isInternal":false,"tokens":1829,"sizeBytes":7352},{"name":"incremental-migration.md","path":"frontend/.claude/skills/tanstack-router-migration/references/incremental-migration.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/tanstack-router-migration/references/incremental-migration.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftanstack-router-migration%2Freferences%2Fincremental-migration.md -->","isInternal":false,"tokens":3196,"sizeBytes":15019},{"name":"migration-patterns.md","path":"frontend/.claude/skills/tanstack-router-migration/references/migration-patterns.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/tanstack-router-migration/references/migration-patterns.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftanstack-router-migration%2Freferences%2Fmigration-patterns.md -->","isInternal":false,"tokens":3671,"sizeBytes":15160},{"name":"route-templates.md","path":"frontend/.claude/skills/tanstack-router-migration/references/route-templates.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/tanstack-router-migration/references/route-templates.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftanstack-router-migration%2Freferences%2Froute-templates.md -->","isInternal":false,"tokens":2905,"sizeBytes":12608},{"name":"SKILL.md","path":"frontend/.claude/skills/tanstack-router-migration/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/tanstack-router-migration/SKILL.md","title":"tanstack-router-migration","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftanstack-router-migration%2FSKILL.md -->","frontmatter":{"name":"tanstack-router-migration","description":"Migrate React applications from React Router to TanStack Router with file-based routing. Use when user requests: (1) Router migration, (2) TanStack Router setup, (3) File-based routing implementation, (4) React Router replacement, (5) Type-safe routing, or mentions 'migrate router', 'tanstack router', 'file-based routes'.","allowed-tools":"Read, Write, Edit, Bash, Glob, Grep, Task","allowedTools":["Read,","Write,","Edit,","Bash,","Glob,","Grep,","Task"]},"allowedTools":["Read,","Write,","Edit,","Bash,","Glob,","Grep,","Task"],"isInternal":false,"tokens":4113,"sizeBytes":16765},{"name":"mock-transport.md","path":"frontend/.claude/skills/testing/rules/mock-transport.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/testing/rules/mock-transport.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2Frules%2Fmock-transport.md -->","frontmatter":{"title":"Mock Transport","impact":"HIGH","impactDescription":"Use createRouterTransport to mock gRPC/Connect API calls","tags":"mocking, api, connect, grpc"},"isInternal":false,"tokens":501,"sizeBytes":2108},{"name":"no-ui-rendering-tests.md","path":"frontend/.claude/skills/testing/rules/no-ui-rendering-tests.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/testing/rules/no-ui-rendering-tests.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2Frules%2Fno-ui-rendering-tests.md -->","frontmatter":{"title":"No UI Rendering Tests","impact":"MEDIUM","impactDescription":"UI component rendering is tested in the UI registry, not in feature code","tags":"ui, rendering, registry"},"isInternal":false,"tokens":553,"sizeBytes":2393},{"name":"test-mock-patterns.md","path":"frontend/.claude/skills/testing/rules/test-mock-patterns.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/testing/rules/test-mock-patterns.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2Frules%2Ftest-mock-patterns.md -->","frontmatter":{"title":"Mock Patterns","impact":"HIGH","impactDescription":"Proper mocking prevents test pollution and enables isolated testing","tags":"rstest, mocking, rs.mock, rs.mocked"},"isInternal":false,"tokens":489,"sizeBytes":1956},{"name":"test-unit-vs-integration.md","path":"frontend/.claude/skills/testing/rules/test-unit-vs-integration.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/testing/rules/test-unit-vs-integration.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2Frules%2Ftest-unit-vs-integration.md -->","frontmatter":{"title":"Unit vs integration tests","impact":"CRITICAL","tags":"bun, rstest, unit, integration"},"isInternal":false,"tokens":187,"sizeBytes":813},{"name":"test-zustand-stores.md","path":"frontend/.claude/skills/testing/rules/test-zustand-stores.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/testing/rules/test-zustand-stores.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2Frules%2Ftest-zustand-stores.md -->","frontmatter":{"title":"Zustand Store Testing","impact":"HIGH","impactDescription":"Prevents state leakage between tests","tags":"rstest, zustand, state, testing"},"isInternal":false,"tokens":449,"sizeBytes":1842},{"name":"SKILL.md","path":"frontend/.claude/skills/testing/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/testing/SKILL.md","title":"testing","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Ftesting%2FSKILL.md -->","frontmatter":{"name":"testing","description":"Write and maintain tests with Bun units and Rstest integrations, mock utilities, and Zustand store testing patterns."},"isInternal":false,"tokens":454,"sizeBytes":1885},{"name":"icon-system.md","path":"frontend/.claude/skills/ui-development/rules/icon-system.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/ui-development/rules/icon-system.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Ficon-system.md -->","frontmatter":{"title":"Icon System","impact":"MEDIUM","impactDescription":"Use central icon imports from components/icons","tags":"icons, lucide, imports"},"isInternal":false,"tokens":441,"sizeBytes":1722},{"name":"registry-upgrade.md","path":"frontend/.claude/skills/ui-development/rules/registry-upgrade.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/ui-development/rules/registry-upgrade.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Fregistry-upgrade.md -->","frontmatter":{"title":"Upgrading the UI Registry","impact":"HIGH","impactDescription":"A registry pull silently overwrites local component changes and can retire tokens without any build error","tags":"registry, upgrade, tokens, sync, design-system"},"isInternal":false,"tokens":1594,"sizeBytes":5754},{"name":"style-no-margin-on-registry.md","path":"frontend/.claude/skills/ui-development/rules/style-no-margin-on-registry.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/ui-development/rules/style-no-margin-on-registry.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Fstyle-no-margin-on-registry.md -->","frontmatter":{"title":"No Margin on Registry Components","impact":"HIGH","impactDescription":"Direct margin classes break component encapsulation and cause layout inconsistencies","tags":"styling, margin, layout, tailwind"},"isInternal":false,"tokens":312,"sizeBytes":1339},{"name":"style-use-tokens.md","path":"frontend/.claude/skills/ui-development/rules/style-use-tokens.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/ui-development/rules/style-use-tokens.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Fstyle-use-tokens.md -->","frontmatter":{"title":"Use Theme Tokens, Not Raw Colours or Stock Text Sizes","impact":"HIGH","impactDescription":"Raw palette classes ignore the theme and break dark mode; stock text sizes bypass the type scale","tags":"styling, tokens, colour, typography, design-system, theme"},"isInternal":false,"tokens":1486,"sizeBytes":5370},{"name":"style-use-variants.md","path":"frontend/.claude/skills/ui-development/rules/style-use-variants.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/ui-development/rules/style-use-variants.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Fstyle-use-variants.md -->","frontmatter":{"title":"Use Component Variants","impact":"HIGH","impactDescription":"Using className overrides breaks design consistency and component updates","tags":"styling, variants, props, design-system"},"isInternal":false,"tokens":538,"sizeBytes":2188},{"name":"use-ui-registry.md","path":"frontend/.claude/skills/ui-development/rules/use-ui-registry.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/ui-development/rules/use-ui-registry.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2Frules%2Fuse-ui-registry.md -->","frontmatter":{"title":"Use UI Registry Components","impact":"CRITICAL","impactDescription":"Using deprecated library causes inconsistent UI and maintenance burden","tags":"components, registry, ui, imports"},"isInternal":false,"tokens":323,"sizeBytes":1340},{"name":"SKILL.md","path":"frontend/.claude/skills/ui-development/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/ui-development/SKILL.md","title":"ui-development","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fui-development%2FSKILL.md -->","frontmatter":{"name":"ui-development","description":"Build UI with Redpanda Registry components, Tailwind v4, and accessibility best practices."},"isInternal":false,"tokens":614,"sizeBytes":2446},{"name":"SKILL.md","path":"frontend/.claude/skills/web-design-guidelines/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/skills/web-design-guidelines/SKILL.md","title":"web-design-guidelines","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fskills%2Fweb-design-guidelines%2FSKILL.md -->","frontmatter":{"description":"Review UI code for Web Interface Guidelines compliance","argument-hint":"<file-or-pattern>"},"isInternal":false,"tokens":1686,"sizeBytes":6828},{"name":"playwright-test-generator.md","path":"frontend/.claude/agents/playwright-test-generator.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/agents/playwright-test-generator.md","title":"Subagent: playwright-test-generator","category":"subagent-persona","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fagents%2Fplaywright-test-generator.md -->","isInternal":false,"tokens":868,"sizeBytes":3749},{"name":"playwright-test-healer.md","path":"frontend/.claude/agents/playwright-test-healer.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/agents/playwright-test-healer.md","title":"Subagent: playwright-test-healer","category":"subagent-persona","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fagents%2Fplaywright-test-healer.md -->","isInternal":false,"tokens":781,"sizeBytes":3748},{"name":"playwright-test-planner.md","path":"frontend/.claude/agents/playwright-test-planner.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/agents/playwright-test-planner.md","title":"Subagent: playwright-test-planner","category":"subagent-persona","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fagents%2Fplaywright-test-planner.md -->","isInternal":false,"tokens":1058,"sizeBytes":4915},{"name":"redpanda-logo-fetcher.md","path":"frontend/.claude/agents/redpanda-logo-fetcher.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/frontend/.claude/agents/redpanda-logo-fetcher.md","title":"Subagent: redpanda-logo-fetcher","category":"subagent-persona","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=frontend%2F.claude%2Fagents%2Fredpanda-logo-fetcher.md -->","isInternal":false,"tokens":5191,"sizeBytes":21175},{"name":"update.md","path":".claude/commands/changelog/update.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/.claude/commands/changelog/update.md","title":"Command: /update","category":"command-prompt","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=.claude%2Fcommands%2Fchangelog%2Fupdate.md -->","frontmatter":{"allowed-tools":"Bash(git tag:*), Bash(git log:*), Bash(git status:*), Bash(git branch:*), Bash(head:*), Bash(pwd), Read, Edit","description":"Generate a changelog comparing a specified tag (or latest if not provided) with the previous version.","allowedTools":["Bash(git","tag:*),","Bash(git","log:*),","Bash(git","status:*),","Bash(git","branch:*),","Bash(head:*),","Bash(pwd),","Read,","Edit"]},"allowedTools":["Bash(git","tag:*),","Bash(git","log:*),","Bash(git","status:*),","Bash(git","branch:*),","Bash(head:*),","Bash(pwd),","Read,","Edit"],"isInternal":false,"tokens":1567,"sizeBytes":6671},{"name":"fix-snyk-vulnerabilities.md","path":".claude/commands/fix-snyk-vulnerabilities.md","rawUrl":"https://raw.githubusercontent.com/redpanda-data/console/HEAD/.claude/commands/fix-snyk-vulnerabilities.md","title":"Command: /fix-snyk-vulnerabilities","category":"command-prompt","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/redpanda-data/console?path=.claude%2Fcommands%2Ffix-snyk-vulnerabilities.md -->","isInternal":false,"tokens":968,"sizeBytes":3872},{"name":".cursorrules","path":".cursorrules (kubestellar-console.mdc)","rawUrl":"https://raw.githubusercontent.com/PatrickJS/awesome-cursorrules/main/rules/kubestellar-console.mdc","title":"Awesome Cursorrules: kubestellar console","category":"cursor-rule","format":"markdown","content":"---\ndescription: \"KubeStellar Console — Multi-cluster Kubernetes dashboard development rules\"\nglobs: **/*.tsx, **/*.ts, **/*.go, web/src/**/*.ts, web/src/**/*.tsx, pkg/**/*.go, cmd/**/*.go\nalwaysApply: false\n---\n# KubeStellar Console Development Rules\n\n## Project Structure\n- Frontend: React + TypeScript in `/web/`\n- Backend: Go (Fiber v2) in root directory\n- Build: `cd web && npm run build && npm run lint` before every commit\n- Cards: Dashboard card components in `web/src/components/cards/`\n- Hooks: Data fetching hooks in `web/src/hooks/`\n\n## Card Development\n- All data fetching MUST go through `useCache`/`useCached*` hooks\n- Always destructure and pass `isDemoData` and `isRefreshing` to `useCardLoadingState()`\n- Never use demo data during loading: `isDemoFallback && !isLoading`\n- Hook ordering: `useCardLoadingState` AFTER hooks providing `isDemoData`\n\n## Array Safety\n- NEVER call `.join()`, `.map()`, `.filter()`, `.forEach()`, `for...of` on values that might be undefined\n- Always guard: `(data || []).map(...)` or `(data || []).join(', ')`\n\n## No Magic Numbers\n- Numeric literals should be named constants, except trivial literals (`0`, `1`, `-1`) in clear local contexts\n- Use constants from `lib/constants/` (time.ts, network.ts, ui.ts)\n\n## Styling\n- Tailwind CSS with `cn()` utility for merging classNames\n- Never use raw hex colors — use semantic Tailwind classes (`text-foreground`, `bg-primary`, `bg-card`)\n- Status colors: `text-green-400`/`bg-green-500/10` (success), `text-yellow-400`/`bg-yellow-500/10` (warning), `text-red-400`/`bg-red-500/10` (error), `text-cyan-400`/`bg-cyan-500/10` (info) — these map to the design system's semantic status tokens and are the only permitted palette classes\n\n## Internationalization\n- All user-facing strings use `t()` from `react-i18next`\n- Keys in `web/src/locales/en/` JSON files\n- Never use raw strings for UI text\n\n## Go Backend\n- Fiber v2 handlers: `func(c *fiber.Ctx) error`\n- Use `fiber.NewError(statusCode, message)` for errors\n- Always `make([]T, 0)` not `var x []T` (nil → null in JSON)\n- Use `log/slog` for structured logging\n- Multi-cluster queries use goroutines + sync.WaitGroup\n\n## Cluster Deduplication\n- Always use `DeduplicatedClusters()` when iterating clusters\n- Multiple kubeconfig contexts can point to same physical cluster\n","tokens":620,"sizeBytes":2319}],"systemPromptSnippet":"<yakaai_skills repo=\"redpanda-data/console\">\n<!-- File: AGENTS.md (Tokens: ~719 | Category: root-instruction) -->\n# Redpanda Console\n\nGo backend (`backend/`) + React 19 single-page app (`frontend/`) sharing a protobuf API\ncontract (`proto/`). Builds and tooling are driven by [Task](https://taskfile.dev/) from the\nrepository root.\n\n## Which rules apply where\n\nRead the nearest file to what you are changing. The nested files own their area; this file\nonly routes.\n\n| You are changing | Read |\n|---|---|\n| `backend/**` | [backend/AGENTS.md](backend/AGENTS.md) |\n| `frontend/**` | [frontend/AGENTS.md](frontend/AGENTS.md) |\n| `proto/**` | this file, plus `buf.yaml` and `taskfiles/proto.yaml` |\n| root config, `taskfiles/**`, `.github/**` | this file |\n| commit messages, PR description, review | [.github/copilot-instructions.md](.github/copilot-instructions.md) and [CONTRIBUTING.md](CONTRIBUTING.md) |\n\n## Commands\n\nBackend and proto commands run **from the repository root** via `task`. The `./taskw` wrapper\npins the Go toolchain and helper binaries under `build/`.\n\n```bash\ntask backend:fmt            # format\ntask backend:lint           # golangci-lint\ntask backend:test-unit      # unit tests\ntask backend:test-integration   # unit + integration (needs Docker)\ntask backend:verify         # lint + generate + integration tests\ntask proto:lint             # buf lint\ntask proto:generate         # regenerate backend + frontend protogen\n```\n\nFrontend commands run **from `frontend/`** via `bun run <script>`.\n\n## Never edit (generated or vendored)\n\n- `backend/pkg/protogen/` and `frontend/src/protogen/` (regenerate with `task proto:generate`)\n- `frontend/src/routeTree.gen.ts` (TanStack Router)\n- `frontend/src/components/redpanda-ui/` (UI Registry, sourced from `redpanda-data/ui-registry`)\n- `build/` (pinned toolchain)\n\n## Enterprise boundary\n\nThis repository is the **OSS foundation**. `console-enterprise` imports\n`github.com/redpanda-data/console/backend` and extends it through the hooks system in\n`backend/pkg/api/hooks.go`, which lets it add interceptors, change routes, and replace service\nimplementations.\n\nFeatures that live behind those hooks (authentication, RBAC, secret management, shadow links,\npipelines, debug bundles) **cannot be exercised from this repository alone**. To test an OSS\nchange against enterprise, add a replace directive in `console-enterprise/backend/go.mod`:\n\n```go\nreplace github.com/redpanda-data/console/backend => /path/to/console/backend\n```\n\n## Proto changes\n\n`proto/` is published to the Buf Schema Registry as `buf.build/redpandadata/dataplane`. A\nchange here is an API contract change:\n\n1. Edit the `.proto` file.\n2. `task proto:lint && task proto:format`\n3. `task proto:generate` and commit the generated output. `proto-generate.yml` fails the PR if\n   generated code is out of date.\n4. Breaking-change detection runs in CI. Some alpha packages are exempt; check the `ignore`\n   list in `buf.yaml` before assuming yours is. Everything else is wire-compatible-only.\n\n<!-- File: backend/AGENTS.md (Tokens: ~1988 | Category: root-instruction) -->\n# Redpanda Console Backend\n\nGo HTTP API server for managing and debugging Kafka/Redpanda workloads. Serves a legacy REST\nAPI under `/api` and a Connect RPC API re-exposed as JSON through gRPC-Gateway under\nversioned paths.\n\nModule: `github.com/redpanda-data/console/backend`. See [../AGENTS.md](../AGENTS.md) for\nrepo-wide rules.\n\n**How to use this file.** It tells you the rules you cannot infer and where to look for the\nrest. It is not a specification. Before relying on a specific, read the source: the linter\nconfig for lint rules, the Taskfile for commands, `routes.go` for what is wired, and the\npackage you are editing for its own patterns. When this file and the code disagree, the code\nwins and this file needs a fix.\n\n## Hard rules\n\nNon-obvious and enforced. Everything else the linter will tell you.\n\n**Errors.** Construct Connect errors through `apierrors`\n([pkg/api/connect/errors/](pkg/api/connect/errors/)), never `connect.NewError` directly, or\nyou lose the error details the helper always attaches. That package also holds converters for\nKafka, Schema Registry, and Redpanda Admin API errors; check for one before writing your own.\nLegacy REST handlers use a different model, `*rest.Error` with `rest.SendRESTError`.\n\n**Logging.** `log/slog` only, and `sloglint` is strict:\n\n- No global logger. Take a `*slog.Logger` as a constructor parameter and store it.\n- Attributes only: `slog.String(\"topic_name\", name)`, never loose key/value pairs.\n- snake_case keys, lowercase messages, and `\"error\"` rather than `\"err\"`.\n- Sub-loggers via `loggerpkg.Named(parent, \"topic_service\")`.\n\n**Doc comments.** `revive` runs with `enable-all-rules: true`, so every exported identifier\nneeds a doc comment.\n\n**License header.** Every new `.go` file starts with the BSL block:\n\n```go\n// Copyright 2026 Redpanda Data, Inc.\n//\n// Use of this software is governed by the Business Source License\n// included in the file licenses/BSL.md\n//\n// As of the Change Date specified in that file, in accordance with\n// the Business Source License, use of this software will be governed\n// by the Apache License, Version 2.0\n```\n\n**Imports.** Three blocks: standard library, third-party, then\n`github.com/redpanda-data/console/backend`. `task backend:fmt` sorts them.\n\n**Generated code.** Never hand-edit `pkg/protogen/`. Run `task proto:generate` from the\nrepository root.\n\nComplexity limits are enforced too, but you will hit those as lint failures rather than\nneeding to plan for them. The full linter set is in [.golangci.yaml](.golangci.yaml).\n\n## Comments\n\nKeep comments to a minimum. Less is more.\n\n- Doc comments on exported identifiers are required, as above. One line is enough.\n- Do not restate the code. `// increment the offset` above `offset++` is noise.\n- No banner comments (`// --- helpers ---`), no labelling the obvious.\n- Comment the **why**: a non-obvious constraint, a protocol quirk, a deliberate deviation, a\n  reason an ordering matters. Link the ticket or upstream issue if there is one.\n- Prefer a better name or a small extracted function over a paragraph of explanation.\n- No commented-out code. `TODO`/`FIXME` must name a ticket or an owner.\n\nDo not add comments to code you touched just to show what changed. That goes in the commit\nmessage.\n\n## Commands\n\nFrom the **repository root**. `task --list` has the rest.\n\n```bash\ntask backend:fmt                # format\ntask backend:lint               # golangci-lint\ntask backend:test-unit          # unit tests\ntask backend:test-integration   # unit + integration, needs Docker\ntask backend:verify             # lint, generate, integration tests\ntask backend:generate           # go generate, proto, format\n```\n\n`CLI_ARGS` is forwarded: `task backend:test-integration -- ./pkg/api/...`.\n\nTo invoke `go` directly, run it **from `backend/`**, since the commands are module-relative:\n\n```bash\ngo test ./pkg/serde/... -run TestJsonSerde\ngo test --tags=integration ./pkg/api/connect/integration/... -run TestSuite/TestTopicV1\n```\n\nRun the server from `backend/`, with a local cluster from `docs/local/`\n(`docker compose up -d`):\n\n```bash\nKAFKA_BROKERS=localhost:9092 SERVEFRONTEND=false SERVER_LISTENPORT=9090 go run cmd/api/main.go\n```\n\n**Before pushing:** `task backend:fmt && task backend:lint && task backend:test-unit`, then\n`task backend:test-integration` if you touched `pkg/api/` or `pkg/console/`.\n\n## Where things are\n\nOnly `cmd/` and `pkg/`. No `internal/`.\n\n| Path | Holds |\n|---|---|\n| `cmd/api/` | the single binary |\n| `pkg/api/` | HTTP and gRPC layer. `api.go` wires dependencies, `routes.go` mounts everything, `hooks.go` is the enterprise extension contract |\n| `pkg/api/connect/service/<domain>/<version>/` | Connect service implementations |\n| `pkg/console/` | core business logic behind the `Servicer` interface |\n| `pkg/factory/` | Kafka, Redpanda Admin, and Schema Registry client factories, consumed as interfaces |\n| `pkg/serde/` | auto-detecting serialization for Kafka messages |\n| `pkg/config/` | koanf config, one file per sub-struct |\n| `pkg/logger/` | slog setup with functional options |\n| `pkg/testutil/` | testcontainers helpers; image tags come from `test-images.json` at the repo root |\n| `pkg/protogen/` | generated, do not edit |\n\n`routes.go` is the map. It shows which services exist, which API versions are mounted, and\nwhich are OSS placeholders that enterprise replaces. Read it rather than assuming.\n\n## Conventions\n\n- Wrap errors with context: `fmt.Errorf(\"failed to describe topic: %w\", err)`. Compare with\n  `errors.Is`/`errors.As`, never `==` or string matching.\n- `ctx context.Context` is the first parameter, and never stored in a struct.\n- Accept interfaces, return concrete types. Assert conformance at package level:\n  `var _ dataplanev1connect.TopicServiceHandler = (*Service)(nil)`.\n- Constructors are `NewX(cfg, logger, collaborators...)`, dropping any the service does not\n  need. No setters.\n- A service package splits by role: `service.go` for the RPCs, `mapper.go` for proto\n  conversion, `defaulter.go` for request defaults, `util.go` for error translation. The\n  package is named for the domain even inside a `v1/` directory, so imports are aliased\n  (`topicsvcv1`).\n- `pkg/console` puts one exported operation per file, named in snake_case after the operation.\n- Config sub-structs implement `SetDefaults`, `RegisterFlags`, and `Validate` as needed. Do\n  not add an empty method to complete the set. Read `LoadConfig` in\n  [pkg/config/config.go](pkg/config/config.go) before changing precedence; the order of\n  defaults, flags, file, and env is load-bearing and not what you would guess.\n- Wiring is manual, no DI framework.\n\n## Enterprise boundary\n\nThis is the OSS foundation. `console-enterprise` imports it and extends it through the `Hooks`\ncontract in [pkg/api/hooks.go](pkg/api/hooks.go), which lets it add interceptors, replace OSS\nservice implementations, register additional services, and supply license and feature-flag\ninformation.\n\nChanging a hook signature breaks console-enterprise. Call it out in the PR description.\n\nThere is a stale duplicate of these declarations in `pkg/api/hooks/`. Nothing imports it;\ndo not add to it.\n\n## Testing\n\n- Table-driven is the house pattern: a slice of cases with a `name` and a `validateFn`\n  closure, run through `t.Run(tc.name, ...)`. See\n  [topic/v1/mapper_test.go](pkg/api/connect/service/topic/v1/mapper_test.go).\n- testify: `require` where a failure makes later assertions meaningless, `assert` for value\n  comparisons. Both in one test is normal.\n- Tests live in the same package as the code (`package topic`, not `topic_test`).\n- Unit tests carry no build tag. Integration tests carry `//go:build integration` and the\n  `_integration_test.go` suffix, except in `pkg/api/connect/integration/` where the tag alone\n  marks them.\n- Integration suites use testcontainers-go and testify/suite. See\n  [integration/api_suite_test.go](pkg/api/connect/integration/api_suite_test.go).\n- Mocks are gomock, from a `//go:generate mockgen` directive next to the interface.\n  Regenerate with `task backend:generate`.\n\n<!-- File: frontend/AGENTS.md (Tokens: ~444 | Category: root-instruction) -->\n# Redpanda Console Frontend\n\nReact 19 · Bun · Rsbuild\n\n## Critical Rules\n\n- New code MUST use Registry components (`src/components/redpanda-ui/`), react-hook-form + Zod, Bun unit tests, Rstest integration/browser/E2E tests, and Connect Query\n- See [no-legacy](.claude/skills/code-standards/rules/no-legacy.md) for prohibited patterns\n- `src/protogen/` is generated — DO NOT EDIT\n\n## Commands\n\nAlways use `bun run <script>` for project tools. Never use `bunx`, `npx`, or run tools directly.\n\n| Command | Purpose |\n|---------|---------|\n| `bun start` | Dev server :3000 |\n| `bun run build` | Production build |\n| `bun run test` | Unit, integration, federation and browser checks |\n| `bun run test:unit` | `.test.ts` (Bun) |\n| `bun run test:integration` | `.test.tsx` (happy-dom) |\n| `bun run lint` | Biome/Ultracite linter |\n| `bun run type:check` | TypeScript |\n\n## Verify Changes\n\nAfter changes: `bun run type:check && bun run lint && bun run test`\n\nClassify tests by behavior before choosing filenames. See [test guide](tests/README.md) for full-stack variants, fixtures, coverage and CI.\n\n## Directory Structure\n\n```\nsrc/\n├── components/\n│   ├── pages/          # Feature pages\n│   └── redpanda-ui/    # UI Registry (USE THIS)\n├── react-query/        # Connect Query hooks\n├── hooks/              # Custom hooks\n├── utils/              # Utilities\n└── protogen/           # Generated (DO NOT EDIT)\n```\n\n## Plan Mode\n\n- Extremely concise plans. Sacrifice grammar for concision.\n- End each plan with unresolved questions, if any.\n\n## Compaction\n\nWhen compacting, preserve: modified file list, test commands, and error messages.\n\n<!-- File: backend/CLAUDE.md (Tokens: ~5 | Category: claude-rule) -->\n@AGENTS.md\n\n<!-- File: CLAUDE.md (Tokens: ~5 | Category: claude-rule) -->\n@AGENTS.md\n\n<!-- File: frontend/CLAUDE.md (Tokens: ~5 | Category: claude-rule) -->\n@AGENTS.md\n\n<!-- File: .github/copilot-instructions.md (Tokens: ~1340 | Category: copilot-instructions) -->\n# Copilot instructions: Redpanda Console\n\nGo backend (`backend/`) + React 19 SPA (`frontend/`) sharing a protobuf contract (`proto/`).\nTask drives builds from the repository root.\n\n## Where the rules live\n\nArea rules are **not** repeated here. They live in `AGENTS.md` files and the nearest one to a\nchanged file wins:\n\n| Path | Governing file |\n|---|---|\n| `backend/**` | `backend/AGENTS.md` |\n| `frontend/**` | `frontend/AGENTS.md` |\n| `proto/**`, root config, `taskfiles/**` | `AGENTS.md` |\n| commits, PR description, review conduct | this file |\n\nWhen a review comment enforces an area rule, name the file it came from so the author can\nread the rule in context.\n\n## Commit messages\n\nFormat is `area[/detail]: short description`.\n\nexample:\n```\nbackend: resolve schema IDs in the topic's registry context\n\nBefore this change the message viewer always looked schemas up in the\ndefault context, so a topic bound to a named context returned a decode\nerror. Resolution now takes the topic's context, which also removes the\nspecial case in the sample handler.\n```\n\n- Subject: 50 characters or fewer, lowercase after the prefix, no trailing period.\n- Common areas: `backend`, `frontend`, `proto`, `topics`, `chore`, `deps`.\n- Body: wrap at 72 columns. Explain **why**, not what: the state being moved away from, why\n  it is no longer wanted, and how the new state addresses that. Then any non-obvious\n  technical detail.\n- A commit message must stand alone. No \"in the previous commit\", no references to meetings\n  or threads. That context goes in the PR description.\n- One logical change per commit, and every commit must build on its own so `git bisect`\n  works.\n\nParts of the history use Conventional Commits (`feat(frontend): ...`). `area:` is the\ndocumented standard in `CONTRIBUTING.md` and the form to use for new work.\n\n## Pull requests\n\n- Title follows the same `area: description` convention.\n- The description carries the high-level narrative that individual commits must not depend\n  on, plus examples of what's being added.\n- Fold review fixups back into the commits they modify before merge. Do not leave\n  \"address review feedback\" commits in the final history.\n- `git rebase --keep-base` keeps the diff for reviewers minimal on a force-push.\n- Call out explicitly when a change touches `backend/pkg/api/hooks.go` or `proto/`. Those are\n  the contract surfaces `console-enterprise` builds on, and a signature change there breaks\n  it. (`backend/pkg/api/hooks/` is a stale duplicate that nothing imports.)\n\n## Comments\n\nRepo-wide: keep comments to a minimum. Less is more. Full rules in\n[backend/AGENTS.md](../backend/AGENTS.md#comments).\n\n- Comment the **why**, never the what. A non-obvious constraint, a protocol quirk, a\n  deliberate deviation. Not `// increment the offset` above `offset++`.\n- Doc comments on exported Go identifiers are mandatory (`revive` runs with all rules on) and\n  are the exception to this, not a license to annotate everything.\n- No banner comments, no commented-out code, no comments that exist to describe what the diff\n  changed. That belongs in the commit message.\n- `TODO`/`FIXME` must name a ticket or an owner.\n- Prefer a better name or a small extracted function over a paragraph explaining the code.\n\nWhen reviewing, flag added comments that restate the code and suggest deleting them. This is\nthe most common form of bloat in AI-assisted pull requests.\n\n## Review priorities\n\nIn order:\n\n1. **Correctness.** Wrong behavior, unhandled errors, swallowed errors, nil dereferences,\n   races, resource leaks, missing `ctx` propagation.\n2. **The area rules** in the relevant `AGENTS.md`, especially the \"Hard rules\" section of\n   `backend/AGENTS.md` and the \"Critical Rules\" of `frontend/AGENTS.md`.\n3. **Contract and compatibility.** Proto changes that are not wire-compatible, hook signature\n   changes, API version placement, generated output not regenerated.\n4. **Tests.** New behavior needs a test. Backend prefers table-driven with `t.Run`; frontend\n   splits `.test.ts` (unit, node) from `.test.tsx` (integration, happy-dom).\n5. **Noise.** Comments that restate the code, commented-out code, and dead scaffolding. See\n   Comments above.\n\n## Do not flag\n\n- Anything under a generated or vendored path: `backend/pkg/protogen/`,\n  `frontend/src/protogen/`, `frontend/src/routeTree.gen.ts`,\n  `frontend/src/components/redpanda-ui/`, `build/`. Changes there are either generated output\n  or upstream registry content.\n- Formatting and style nits already enforced by tooling. `golangci-lint` 2.10 (37 linters,\n  `revive` with all rules on, `sloglint`, `gci`) and Biome/Ultracite gate those in CI, and a\n  duplicate review comment adds noise without adding information.\n- Line length, import order, or trailing whitespace. The formatters own those.\n\n## What CI enforces\n\n| Workflow | Gate |\n|---|---|\n| `backend-lint-test.yml` | `golangci-lint` + unit and integration tests |\n| `frontend-verify.yml` | Biome/Ultracite (with a dirty-tree check), `ultracite doctor`, type check, build, tests |\n| `buf.yml` | proto lint, format, and breaking-change detection |\n| `proto-generate.yml` | fails if generated proto output is not committed |\n| `frontend-react-doctor.yml` | React Doctor on frontend changes |\n| `frontend-ui-audit.yml` | advisory only: registry drift, off-token colors, ad-hoc utility classes |\n| `repository-dispatch.yml` | triggers the `console-enterprise` build after verify |\n\nPrefer pointing at the gate that will catch an issue over restating what the gate already\nsays.\n\n<!-- File: frontend/.claude/rules/test-unit-vs-integration.md (Tokens: ~143 | Category: anthropic-skill) -->\n---\npaths:\n  - \"src/**/*.test.{ts,tsx}\"\n---\n\n# Test classification\n\nClassify what the test exercises before choosing its filename:\n\n- Pure logic/mocked module contracts: `.test.ts`, Bun (`bun run test:unit`).\n- React, DOM, hooks, state/transport integration: `.test.tsx`, Rstest + Happy DOM.\n- Configuration/subprocess integration: `.integration.test.ts`, Rstest.\n- Native browser and full running-app journeys: `.spec.ts`, Rstest + Playwright.\n\nSee [test guide](../../tests/README.md) for commands and fixtures. Keep full-stack journeys on Testcontainers; mocked bundle tests do not replace them.\n\n<!-- File: frontend/.claude/skills/api-patterns/rules/api-invalidate-cache.md (Tokens: ~533 | Category: anthropic-skill) -->\n---\ntitle: Invalidate Cache After Mutations\nimpact: HIGH\nimpactDescription: Stale cache after mutations causes UI to show outdated data\ntags: cache, invalidation, mutation, react-query\n---\n\n# Invalidate Cache After Mutations (HIGH)\n\n## Explanation\n\nAfter mutations (create, update, delete), invalidate related queries to ensure the UI reflects the latest server state. Without invalidation, users see stale data until they manually refresh.\n\n## Incorrect\n\n```tsx\n// No invalidation after mutation\nconst mutation = useMutation(updateCluster, {\n  onSuccess: () => {\n    toast.success('Cluster updated');\n    // UI still shows old data!\n  },\n});\n```\n\n```tsx\n// Invalidating wrong query key\nconst mutation = useMutation(updateCluster, {\n  onSuccess: () => {\n    queryClient.invalidateQueries({ queryKey: ['clusters'] }); // Wrong key\n  },\n});\n```\n\n## Correct\n\n```tsx\nimport { useMutation } from '@connectrpc/connect-query';\nimport { useQueryClient } from '@tanstack/react-query';\n\nconst queryClient = useQueryClient();\n\nconst mutation = useMutation(updateCluster, {\n  onSuccess: () => {\n    // Invalidate specific query\n    queryClient.invalidateQueries({\n      queryKey: ['getCluster', clusterId],\n    });\n\n    // Or invalidate all cluster queries\n    queryClient.invalidateQueries({\n      queryKey: ['getClusters'],\n    });\n\n    toast.success('Cluster updated');\n  },\n});\n```\n\n## Optimistic Updates\n\nFor instant UI feedback:\n\n```tsx\nconst mutation = useMutation(updateCluster, {\n  onMutate: async (newData) => {\n    // Cancel outgoing refetches\n    await queryClient.cancelQueries({ queryKey: ['getCluster', id] });\n\n    // Snapshot previous value\n    const previous = queryClient.getQueryData(['getCluster', id]);\n\n    // Optimistically update\n    queryClient.setQueryData(['getCluster', id], newData);\n\n    return { previous };\n  },\n  onError: (err, newData, context) => {\n    // Rollback on error\n    queryClient.setQueryData(['getCluster', id], context?.previous);\n  },\n  onSettled: () => {\n    // Always refetch after mutation settles\n    queryClient.invalidateQueries({ queryKey: ['getCluster', id] });\n  },\n});\n```\n\n## Reference\n\n- [Query Invalidation](https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation)\n- [Optimistic Updates](https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates)\n\n<!-- File: frontend/.claude/skills/api-patterns/rules/api-toast-errors.md (Tokens: ~425 | Category: anthropic-skill) -->\n---\ntitle: Toast Error Messages\nimpact: HIGH\nimpactDescription: Consistent error messages help users understand and recover from failures\ntags: error-handling, toast, ux, mutations\n---\n\n# Toast Error Messages (HIGH)\n\n## Explanation\n\nUse `formatToastErrorMessage` utility for consistent, user-friendly error messages in mutation `onError` handlers. This provides a standard format that includes the action, entity, and error details.\n\n## Incorrect\n\n```tsx\n// Generic error message\nconst mutation = useMutation(deleteCluster, {\n  onError: () => {\n    toast.error('Something went wrong');\n  },\n});\n\n// Raw error object\nconst mutation = useMutation(deleteCluster, {\n  onError: (error) => {\n    toast.error(error.message); // May be technical/unhelpful\n  },\n});\n\n// No error handling\nconst mutation = useMutation(deleteCluster, {\n  onSuccess: () => toast.success('Deleted'),\n  // Missing onError!\n});\n```\n\n## Correct\n\n```tsx\nimport { formatToastErrorMessage } from 'utils/toast.utils';\n\nconst mutation = useMutation(deleteCluster, {\n  onSuccess: () => {\n    toast.success('Cluster deleted');\n  },\n  onError: (error) => {\n    toast.error(formatToastErrorMessage({\n      error: {\n        reason: error.message,\n        internalCode: error.code,\n      },\n      action: 'delete',\n      entity: 'cluster',\n    }));\n  },\n});\n\n// Output: \"Failed to delete cluster due to: reason (code: 123)\"\n```\n\n## Standard Actions\n\n| Action | Use For |\n|--------|---------|\n| `create` | POST / create operations |\n| `update` | PUT / PATCH operations |\n| `delete` | DELETE operations |\n| `fetch` | GET operations (rare) |\n\n## Error Boundaries for Render Errors\n\n```tsx\nimport * as Sentry from '@sentry/react';\n\n<Sentry.ErrorBoundary fallback={<ErrorFallback />}>\n  <ClusterDashboard />\n</Sentry.ErrorBoundary>\n```\n\n## Reference\n\n- `src/utils/toast.utils.ts` - Toast utility implementation\n\n<!-- File: frontend/.claude/skills/api-patterns/rules/protobuf-no-edit.md (Tokens: ~404 | Category: anthropic-skill) -->\n---\ntitle: Protobuf No Edit\nimpact: CRITICAL\nimpactDescription: Never edit files in protogen/ - they are auto-generated\ntags: protobuf, generated, code-gen\n---\n\n## Protobuf No Edit\n\n**Impact:** CRITICAL - Never edit files in protogen/ - they are auto-generated\n\nThe `/src/protogen/` directory contains auto-generated TypeScript from protobuf schemas. Any manual edits will be lost on regeneration and may cause type mismatches.\n\n**Incorrect:**\n```typescript\n// WRONG: Editing generated file\n// src/protogen/user_pb.ts\nexport interface User {\n  id: string;\n  name: string;\n  customField: string; // WRONG: Manual addition\n}\n```\n\n**Correct:**\n```typescript\n// CORRECT: Extend types outside protogen\n// src/types/user.ts\nimport { User as ProtoUser } from \"protogen/user_pb\";\n\nexport interface ExtendedUser extends ProtoUser {\n  customField: string;\n}\n\n// CORRECT: Transform data in hooks/utils\n// src/hooks/useUser.ts\nconst { data } = useQuery(getUser, { id });\n\nconst extendedUser = useMemo(() => ({\n  ...data,\n  displayName: `${data?.firstName} ${data?.lastName}`,\n}), [data]);\n```\n\n## Regenerating Protos\n\nWhen proto schemas change upstream:\n\n```bash\n# From repository root (not frontend/)\ntask proto:generate\n```\n\n## Finding Generated Files\n\n```\nsrc/protogen/\n├── *_pb.ts           # Message types\n├── *_connect.ts      # Service definitions\n└── *_connectquery.ts # Connect Query hooks\n```\n\n## Common Imports\n\n```typescript\n// Message types\nimport { User, CreateUserRequest } from \"protogen/user_pb\";\n\n// Service methods\nimport { UserService } from \"protogen/user_connect\";\n\n// Query hooks\nimport { getUser, listUsers, createUser } from \"protogen/user-UserService_connectquery\";\n```\n\n<!-- File: frontend/.claude/skills/api-patterns/rules/use-connect-query.md (Tokens: ~509 | Category: anthropic-skill) -->\n---\ntitle: Use Connect Query\nimpact: CRITICAL\nimpactDescription: Consistent API patterns enable caching, refetching, and error handling\ntags: connect-query, react-query, api, grpc\n---\n\n# Use Connect Query (CRITICAL)\n\n## Explanation\n\nAlways use Connect Query hooks (`useQuery`, `useMutation`) for API calls. These integrate with React Query for caching, background refetching, and optimistic updates. Never use raw fetch or manual state management for server data.\n\n## Incorrect\n\n```tsx\n// Manual fetch with useState\nfunction ClusterList() {\n  const [clusters, setClusters] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    fetch('/api/clusters')\n      .then(res => res.json())\n      .then(data => {\n        setClusters(data);\n        setLoading(false);\n      });\n  }, []);\n}\n```\n\n```tsx\n// Missing enabled flag\nconst { data } = useQuery(\n  getClusters,\n  { organizationId: undefined } // Will error!\n);\n```\n\n## Correct\n\n```tsx\nimport { useQuery } from '@connectrpc/connect-query';\nimport { getClusters } from 'proto/frontend/controlplane/v1/cluster-ClusterService_connectquery';\n\nfunction ClusterList({ organizationId }: Props) {\n  const {\n    data: clusters,\n    isLoading,\n    error,\n    refetch,\n  } = useQuery(\n    getClusters,\n    { organizationId },\n    {\n      staleTime: 1000 * 60 * 5, // 5 minutes\n      enabled: !!organizationId, // Only fetch when ID exists\n    }\n  );\n\n  if (isLoading) return <Skeleton />;\n  if (error) return <ErrorDisplay error={error} />;\n\n  return <List items={clusters} />;\n}\n```\n\n## Common Options\n\n| Option | Purpose |\n|--------|---------|\n| `enabled` | Conditional fetching |\n| `staleTime` | Cache duration before refetch |\n| `refetchInterval` | Polling interval |\n| `refetchOnWindowFocus` | Refetch when tab focused |\n\n## Reference\n\n- [Connect Query Docs](https://connectrpc.com/docs/web/query/getting-started)\n- [TanStack Query Options](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery)\n- See also: [use-react-query-for-server](../../state-management/rules/use-react-query-for-server.md) — when to use React Query vs Zustand\n\n<!-- File: frontend/.claude/skills/api-patterns/SKILL.md (Tokens: ~232 | Category: anthropic-skill) -->\n---\nname: api-patterns\ndescription: Connect Query patterns for API calls. Use when working with mutations, queries, or data fetching.\n---\n\n# API Patterns\n\nMake API calls with Connect Query and handle responses properly.\n\n## Activation Conditions\n\n- Making API calls\n- Using Connect Query hooks\n- Cache invalidation\n- Mutations and optimistic updates\n- Toast notifications for errors\n\n## Quick Reference\n\n| Action | Rule |\n|--------|------|\n| Fetch data | `use-connect-query.md` |\n| After mutation | `api-invalidate-cache.md` |\n| Handle errors | `api-toast-errors.md` (use `formatToastErrorMessage` in onError) |\n| Protobuf files | `protobuf-no-edit.md` |\n\n## Key Locations\n\n| Location | Purpose |\n|----------|---------|\n| `/src/react-query/` | Connect Query hooks |\n| `/src/protogen/` | Generated protos (DO NOT EDIT) |\n\nRegenerate protos: `task proto:generate` (from repo root)\n\n## Rules\n\nSee `rules/` directory for detailed guidance on queries, mutations, cache invalidation, and error handling.\n\n<!-- File: frontend/.claude/skills/code-standards/rules/async-await-promises.md (Tokens: ~484 | Category: anthropic-skill) -->\n---\ntitle: Async/Await Over Promise Chains\nimpact: HIGH\nimpactDescription: async/await is more readable and makes error handling explicit\ntags: async, await, promises, javascript\n---\n\n# Async/Await Over Promise Chains (HIGH)\n\n## Explanation\n\nUse `async/await` instead of `.then()` chains. Async/await is more readable, makes error handling explicit, and avoids callback hell. Never leave promises floating without handling.\n\n## Incorrect\n\n```typescript\n// Promise chains\nfunction loadData() {\n  fetchUser()\n    .then(user => fetchPosts(user.id))\n    .then(posts => processPosts(posts))\n    .catch(error => handleError(error));\n}\n\n// Floating promise (no await)\nfunction save() {\n  saveToDatabase(data); // Promise returned but not awaited!\n}\n\n// Async in Promise executor\nnew Promise(async (resolve) => {\n  const data = await fetch(url);\n  resolve(data);\n});\n\n// Mixing patterns\nasync function mixed() {\n  return fetch(url).then(res => res.json());\n}\n```\n\n## Correct\n\n```typescript\n// async/await\nasync function loadData() {\n  try {\n    const user = await fetchUser();\n    const posts = await fetchPosts(user.id);\n    return processPosts(posts);\n  } catch (error) {\n    handleError(error);\n  }\n}\n\n// Always await or return promises\nasync function save() {\n  await saveToDatabase(data);\n}\n\n// Parallel execution when needed\nasync function loadAll() {\n  const [users, posts] = await Promise.all([\n    fetchUsers(),\n    fetchPosts(),\n  ]);\n  return { users, posts };\n}\n\n// Consistent async/await\nasync function fetchJson(url: string) {\n  const response = await fetch(url);\n  return response.json();\n}\n```\n\n## Error Handling\n\n```typescript\n// Explicit try/catch\nasync function riskyOperation() {\n  try {\n    const result = await mightFail();\n    return result;\n  } catch (error) {\n    // Log and rethrow or handle\n    console.error('Operation failed:', error);\n    throw error;\n  }\n}\n\n// Error boundaries for React\n<ErrorBoundary fallback={<ErrorFallback />}>\n  <AsyncComponent />\n</ErrorBoundary>\n```\n\n## Reference\n\n- [MDN async/await](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Promises)\n\n<!-- File: frontend/.claude/skills/code-standards/rules/no-legacy.md (Tokens: ~530 | Category: anthropic-skill) -->\n---\ntitle: No Legacy Patterns\nimpact: CRITICAL\nimpactDescription: Never use legacy patterns - they block migration efforts\ntags: legacy, migration, mobx, yup\n---\n\n## No Legacy Patterns\n\n**Impact:** CRITICAL - Never use legacy patterns - they block migration efforts\n\nThe codebase is actively migrating away from legacy libraries. Using them in new code blocks migration and creates tech debt.\n\n## Prohibited Patterns\n\n| Legacy | Modern Alternative |\n|--------|-------------------|\n| MobX, decorators | Zustand for global state |\n| Class components | Functional components + hooks |\n| Yup validation | Zod |\n| `any` type | `unknown` or specific types |\n| Jest | Rstest |\n\n## Common Violations\n\n**MobX:**\n```typescript\n// WRONG\nimport { makeAutoObservable, observable, action } from \"mobx\";\nimport { observer } from \"mobx-react-lite\";\n\n@observer\nclass Store {\n  @observable users = [];\n}\n```\n\n```typescript\n// CORRECT\nimport { create } from \"zustand\";\n\nconst useStore = create<State>((set) => ({\n  users: [],\n  addUser: (user) => set((s) => ({ users: [...s.users, user] })),\n}));\n```\n\n**Class Components:**\n```tsx\n// WRONG\nclass UserProfile extends React.Component<Props, State> {\n  componentDidMount() { /* ... */ }\n  render() { return <div />; }\n}\n```\n\n```tsx\n// CORRECT\nconst UserProfile = ({ userId }: Props) => {\n  const { data } = useQuery(getUser, { id: userId });\n  return <Card>{data?.name}</Card>;\n};\n```\n\n## Migration Guidance\n\nWhen modifying files with legacy patterns:\n1. **Don't add** new legacy imports\n2. **Consider migrating** the component if changes are significant\n3. **Keep changes minimal** if just fixing bugs\n4. **New features** must use modern patterns\n\n### MobX → React State: Check Side-Effects\n\nWhen converting observables to React state, automatic behaviors must become explicit:\n- MobX observable arrays auto-update UI when mutated\n- React state requires explicit `setState()` calls\n- Clearing, resetting, or initializing state needs manual handling\n\n**Checklist**: List all observable mutations → add equivalent `setState` calls.\n\n## Legacy Locations\n\n- MobX stores: `src/state/` (do not add new files)\n\n## See Also\n\n- [use-ui-registry](../../ui-development/rules/use-ui-registry.md) — Registry component usage and installation\n\n<!-- File: frontend/.claude/skills/code-standards/rules/react-functional-only.md (Tokens: ~536 | Category: anthropic-skill) -->\n---\ntitle: Functional Components Only\nimpact: HIGH\nimpactDescription: Class components are deprecated patterns that make code harder to test and compose\ntags: react, components, functional, hooks\n---\n\n# Functional Components Only (HIGH)\n\n## Explanation\n\nAlways use functional components with hooks. Class components are a legacy pattern that makes code harder to test, compose, and optimize. Functional components are the modern React standard.\n\n## Incorrect\n\n```tsx\n// Class component\nclass UserProfile extends React.Component<Props, State> {\n  state = { loading: true };\n\n  componentDidMount() {\n    this.fetchUser();\n  }\n\n  render() {\n    return <div>{this.state.loading ? 'Loading...' : this.props.name}</div>;\n  }\n}\n\n// Nested component definition (causes remounting)\nfunction Parent() {\n  function Child() {\n    return <div>Child</div>;\n  }\n  return <Child />; // Child remounts every render!\n}\n\n// Conditional hooks\nfunction Component({ enabled }) {\n  if (enabled) {\n    const [state, setState] = useState(); // Rules of Hooks violation\n  }\n}\n```\n\n## Correct\n\n```tsx\n// Functional component\nexport function UserProfile({ userId }: Props) {\n  const [loading, setLoading] = useState(true);\n  const [user, setUser] = useState<User | null>(null);\n\n  useEffect(() => {\n    fetchUser(userId).then(data => {\n      setUser(data);\n      setLoading(false);\n    });\n  }, [userId]);\n\n  if (loading) return <Skeleton />;\n\n  return <div>{user?.name}</div>;\n}\n\n// Sibling components defined outside\nfunction Parent() {\n  return <Child />;\n}\n\nfunction Child() {\n  return <div>Child</div>;\n}\n\n// Hooks always at top level\nfunction Component({ enabled }: Props) {\n  const [state, setState] = useState<string | null>(null);\n\n  if (!enabled) return null;\n\n  return <div>{state}</div>;\n}\n```\n\n## Component Patterns\n\n```tsx\n// Named export (preferred)\nexport function FeatureComponent() { }\n\n// Colocated sub-components\nexport function Feature() {\n  return (\n    <div>\n      <FeatureHeader />\n      <FeatureContent />\n    </div>\n  );\n}\n\n// Keep sub-components in same file if small\nfunction FeatureHeader() { return <h1>Title</h1>; }\nfunction FeatureContent() { return <p>Content</p>; }\n```\n\n## Reference\n\n- [React Hooks](https://react.dev/reference/react/hooks)\n- [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks)\n\n<!-- File: frontend/.claude/skills/code-standards/SKILL.md (Tokens: ~312 | Category: anthropic-skill) -->\n---\nname: code-standards\ndescription: TypeScript, React, and JavaScript best practices enforced by Ultracite/Biome.\n---\n\n# Code Standards\n\nWrite code that is accessible, performant, type-safe, and maintainable.\n\n## Activation Conditions\n\n- Linting errors need fixing\n- Code formatting issues\n- Code review feedback\n- Questions about style guidelines\n\n## Quick Reference\n\n| Action | Rule |\n|--------|------|\n| Write components | `react-functional-only.md` |\n| Async code | `async-await-promises.md` |\n| Avoid legacy libs | `no-legacy.md` |\n\n`any` types and top-level regex are enforced by Biome (`noExplicitAny`, `useTopLevelRegex`).\n\n## Commands\n\n```bash\nbun x ultracite fix     # Format and fix\nbun x ultracite check   # Check for issues\nbun x ultracite doctor  # Diagnose setup\n```\n\n## Quick Fix\n\nMost issues are auto-fixed:\n\n```bash\nbun x ultracite fix\n```\n\n## Console Logging\n\n- **Never** use `console.log`, `console.debug`, or `console.info` in production code\n- Only use `console.error` and `console.warn` for actionable errors\n- For debug logging, wrap in `if (IsDev) { ... }`\n- Don't use `biome-ignore` to suppress console warnings - remove the logs instead\n\n## Rules\n\nSee `rules/` directory for detailed guidance.\n\n<!-- File: frontend/.claude/skills/e2e-tester/references/container-setup.md (Tokens: ~347 | Category: anthropic-skill) -->\n# Container setup reference\n\nThe maintained lifecycle lives in `tests/shared/global-setup.mjs`, shared by all four\nRstest variant configs. Read [Testcontainers details](../../../../tests/TESTCONTAINERS.md)\nand [test commands](../../../../tests/README.md) before changing it.\n\nBuild the frontend first. Setup copies its assets into the backend build context,\nbuilds an image, starts the configured brokers/services and backend, and waits for\nreadiness. The returned teardown owns live handles; `global-teardown.mjs` provides\nmanual crash recovery using the serialized state. Preserve failure cleanup.\n\nVariant metadata/configuration lives in `tests/test-variant-*/config/`. Images are\npinned in `test-images.json` at the repository root. Local single-variant runs remap host ports;\ncontainer-to-container addresses stay unchanged.\n\nEnterprise requires a separate backend checkout (`ENTERPRISE_BACKEND_DIR`). Global\nsetup authenticates through the real login form and stores browser state under the\ngitignored `playwright/.auth/` directory. Login-error specs override storage with an\nempty state. There is no HTTP Basic-auth replacement for application login.\n\nCI runs three OSS variants in `.github/workflows/frontend-verify.yml`. It installs\nChromium and Linux dependencies, downloads the same-run frontend build, and invokes\n`bun run e2e-test:variant <variant>`. Failure evidence is uploaded from `tests/.rstest/`.\nThe enterprise backend remains the responsibility of downstream enterprise CI.\n\nFor port conflicts, inspect the owner or use the dynamic-port wrapper. Never kill an\nunidentified developer process. For service failures, inspect Docker logs and the\nvariant state file rather than weakening waits or assertions.\n\n<!-- File: frontend/.claude/skills/e2e-tester/references/failure-analysis.md (Tokens: ~983 | Category: anthropic-skill) -->\n# Failure Analysis Reference\n\n## CLI and artifacts\n\nRun `bun run e2e-test:variant console list --summary` to collect tests without services.\nRun `bun run e2e-test:variant console topics/topic-list.spec.ts` to reproduce against the\nreal stack. Inspect `.rstest/` screenshots, first-retry videos and Playwright traces.\nEnterprise screenshots/videos remain disabled. Use native locators/DOM inspection for\nselector diagnosis; do not replace real E2E coverage with mocked component tests.\n\n## Common Failure Patterns\n\n### 1. Element Not Found\n\n```\nError: locator.click: Target closed\nError: Timeout 30000ms exceeded waiting for locator\n```\n\n**Analysis steps:**\n1. Check if element has correct testid/role\n2. Verify element is visible (not hidden/collapsed)\n3. Check for timing issues (element loads async)\n4. Look for dynamic content that changes selector\n\n**Fix:**\n```typescript\n// BAD: Element might not be loaded\nawait page.getByRole('button', { name: 'Create' }).click();\n\n// GOOD: Wait for element to be visible\nawait expect(page.getByRole('button', { name: 'Create' })).toBeVisible();\nawait page.getByRole('button', { name: 'Create' }).click();\n\n// BETTER: Add testid for stability\nawait page.getByTestId('create-button').click();\n```\n\n### 2. Selector Ambiguity\n\n```\nError: strict mode violation: locator('button') resolved to 3 elements\n```\n\n**Fix:**\n```typescript\n// BAD: Multiple \"Edit\" buttons on page\nawait page.getByRole('button', { name: 'Edit' }).click();\n\n// GOOD: More specific with testid\nawait page.getByTestId('schema-edit-compatibility-btn').click();\n\n// GOOD: Scope within container\nawait page.getByRole('region', { name: 'Schema Details' })\n          .getByRole('button', { name: 'Edit' }).click();\n```\n\n### 3. Timing/Race Conditions\n\n```\nError: expect(locator).toHaveText()\nExpected string: \"Success\"\nReceived string: \"Loading...\"\n```\n\n**Fix:**\n```typescript\n// BAD: Doesn't wait for state change\nawait page.getByRole('button', { name: 'Save' }).click();\nexpect(page.getByText('Success')).toBeVisible();\n\n// GOOD: Wait for the expected state\nawait page.getByRole('button', { name: 'Save' }).click();\nawait expect(page.getByText('Success')).toBeVisible({ timeout: 5000 });\n```\n\n### 4. Navigation Issues\n\n```\nError: page.goto: net::ERR_CONNECTION_REFUSED\n```\n\n**Fix:**\n```bash\n# Check containers are running\ndocker ps | grep console-backend\n\n# Check container logs\ndocker logs <container-id>\n\n# Verify port mapping\ncurl http://localhost:3000\n\n# Check testcontainer state file\ncat .testcontainers-state.json\n```\n\n## Systematic Failure Analysis Workflow\n\n1. **Get Test Results** — Run the relevant variant CLI and inspect console/artifact output\n2. **Analyze Error Patterns:**\n   - Selector not found -> Missing/wrong testid or element not visible\n   - Strict mode violation -> Need more specific selector\n   - Timeout -> Element loads async, need waitFor\n   - Connection refused -> Container/service not running\n3. **Find Missing Test IDs** — Inspect semantic roles and labels in the source and browser\n4. **Add Test IDs** — Read component, add `data-testid`, follow naming convention\n5. **Update Tests** — Replace brittle selectors with stable testids, add wait conditions\n6. **Verify Fixes** — Run specific test file, then full suite\n\n## Debugging Commands\n\n```bash\n# Check container logs\ndocker ps -a | grep console-backend\ndocker logs <container-id>\n\n# Check if services are accessible\ncurl http://localhost:3000\ncurl http://localhost:19092\n\n# Run with debug output\nDEBUG=pw:api bun run e2e-test\n\n# Keep containers running on failure\nTESTCONTAINERS_RYUK_DISABLED=true bun run e2e-test\n```\n\n**In-test debugging:**\n```typescript\nawait page.pause();  // Opens Playwright Inspector\nawait page.screenshot({ path: 'debug.png' });\nconsole.log(await page.content());\n```\n\n## Test Timeout Issues\n\n```typescript\n// Increase timeout for slow operations\ntest('slow operation', async ({ page }) => {\n  test.setTimeout(60000); // 60 seconds\n  await page.goto('/slow-page');\n  await expect(page.getByText('Loaded')).toBeVisible({ timeout: 30000 });\n});\n```\n\n<!-- File: frontend/.claude/skills/e2e-tester/references/test-patterns.md (Tokens: ~1096 | Category: anthropic-skill) -->\n# Test Patterns Reference\n\n## Basic Test Structure\n\n```typescript\nimport { test, expect } from '../shared/e2e';\n\ntest.describe('Feature Name', () => {\n  test('user can complete workflow', async ({ page }) => {\n    await page.goto('/feature');\n    await page.getByRole('button', { name: 'Create' }).click();\n    await page.getByLabel('Name').fill('test-item');\n    await page.getByRole('button', { name: 'Submit' }).click();\n    await expect(page.getByText('Success')).toBeVisible();\n    await expect(page).toHaveURL(/\\/feature\\/test-item/);\n  });\n});\n```\n\n## Multi-Step Workflows\n\n```typescript\ntest('user creates, configures, and tests topic', async ({ page }) => {\n  // Step 1: Navigate and create\n  await page.goto('/topics');\n  await page.getByRole('button', { name: 'Create Topic' }).click();\n\n  // Step 2: Fill form\n  await page.getByLabel('Topic Name').fill('test-topic');\n  await page.getByLabel('Partitions').fill('3');\n  await page.getByRole('button', { name: 'Create' }).click();\n\n  // Step 3: Verify creation\n  await expect(page.getByText('Topic created successfully')).toBeVisible();\n  await expect(page).toHaveURL(/\\/topics\\/test-topic/);\n\n  // Step 4: Configure topic\n  await page.getByRole('button', { name: 'Configure' }).click();\n  await page.getByLabel('Retention Hours').fill('24');\n  await page.getByRole('button', { name: 'Save' }).click();\n\n  // Step 5: Verify configuration\n  await expect(page.getByText('Configuration saved')).toBeVisible();\n});\n```\n\n## Testing Forms\n\n```typescript\ntest('form validation works correctly', async ({ page }) => {\n  await page.goto('/create-topic');\n\n  // Submit empty form - should show errors\n  await page.getByRole('button', { name: 'Create' }).click();\n  await expect(page.getByText('Name is required')).toBeVisible();\n\n  // Fill valid data - should succeed\n  await page.getByLabel('Topic Name').fill('valid-topic');\n  await page.getByRole('button', { name: 'Create' }).click();\n  await expect(page.getByText('Success')).toBeVisible();\n});\n```\n\n## Testing Data Tables\n\n```typescript\ntest('user can filter and sort topics', async ({ page }) => {\n  await page.goto('/topics');\n\n  // Filter\n  await page.getByPlaceholder('Search topics').fill('test-');\n  await expect(page.getByRole('row')).toHaveCount(3); // Header + 2 results\n\n  // Sort\n  await page.getByRole('columnheader', { name: 'Name' }).click();\n  const firstRow = page.getByRole('row').nth(1);\n  await expect(firstRow).toContainText('test-topic-a');\n});\n```\n\n## API Interactions\n\n```typescript\ntest('creating topic triggers backend API', async ({ page }) => {\n  // Listen for API call\n  const apiPromise = page.waitForResponse(\n    resp => resp.url().includes('/api/topics') && resp.status() === 201\n  );\n\n  // Trigger action\n  await page.goto('/topics');\n  await page.getByRole('button', { name: 'Create Topic' }).click();\n  await page.getByLabel('Name').fill('api-test-topic');\n  await page.getByRole('button', { name: 'Create' }).click();\n\n  // Verify API was called\n  const response = await apiPromise;\n  const body = await response.json();\n  expect(body.name).toBe('api-test-topic');\n});\n```\n\n## Async Operations\n\n```typescript\n// Wait for specific condition\nawait expect(page.getByRole('status')).toHaveText('Ready');\n\n// Wait for navigation\nawait page.waitForURL('**/topics/my-topic');\n\n// Wait for API response\nawait page.waitForResponse(resp =>\n  resp.url().includes('/api/topics') && resp.status() === 200\n);\n\n// NEVER use fixed timeouts\n// await page.waitForTimeout(5000); // BAD\n```\n\n## Selectors Best Practices\n\n```typescript\n// GOOD: Role-based (accessible)\npage.getByRole('button', { name: 'Create Topic' })\npage.getByLabel('Topic Name')\npage.getByText('Success message')\n\n// GOOD: Test IDs when role isn't clear\npage.getByTestId('topic-list-item')\n\n// BAD: CSS selectors (brittle)\npage.locator('.btn-primary')\npage.locator('#topic-name-input')\n```\n\n## Adding Test IDs to Components\n\n```tsx\n// In React component\n<Button data-testid=\"create-topic-button\">Create Topic</Button>\n\n// In test\nawait page.getByTestId('create-topic-button').click();\n```\n\n**Where to Add:**\n1. Primary actions: Create, Save, Delete, Edit, Submit, Cancel buttons\n2. Navigation: Links to detail pages, breadcrumbs\n3. Forms: All input fields, selects, checkboxes, radio buttons\n4. Lists/Tables: Row identifiers, action buttons within rows\n5. Dialogs/Modals: Open/close buttons, form elements inside\n6. Search/Filter: Search inputs, filter dropdowns, clear buttons\n\n<!-- File: frontend/.claude/skills/e2e-tester/SKILL.md (Tokens: ~851 | Category: anthropic-skill) -->\n---\nname: e2e-tester\ndescription: \"Write and run Rstest/Playwright E2E tests for Redpanda Console using testcontainers. Analyzes test failures, adds missing testids, and improves test stability. Use when user requests E2E tests, Playwright tests, integration tests, test failures, missing testids, or mentions 'test workflow', 'browser testing', 'end-to-end', or 'testcontainers'.\"\nallowed-tools: Read, Write, Edit, Bash, Glob, Grep, Task, mcp__ide__getDiagnostics\n---\n\n# E2E testing with Rstest, Playwright and Testcontainers\n\nWrite end-to-end tests using Playwright against a full Redpanda Console stack running in Docker containers via testcontainers.\n\n## When to Use This Skill\n\n- Testing 2+ step user journeys (login -> action -> verify)\n- Multi-page workflows\n- Browser automation with Playwright\n\n**NOT for:** Component unit tests -> use [testing](../testing/SKILL.md)\n\n## Critical Rules\n\n**ALWAYS:**\n- Run `bun run build` before running E2E tests (frontend assets required)\n- Use `testcontainers` API for container management (never manual `docker` commands in tests)\n- Use `page.getByRole()` and `page.getByLabel()` selectors (avoid CSS selectors)\n- Add `data-testid` attributes when semantic selectors aren't available\n- Run Rstest CLI commands and inspect `.rstest/` artifacts to analyze failures\n- Clean up test data using `afterEach` to call cleanup API endpoints\n\n**NEVER:**\n- Test UI component rendering (use unit/integration tests)\n- Use brittle CSS selectors like `.class-name` or `#id`\n- Use `force:true` on `.click()` or `waitForTimeout`\n- Hard-code wait times (use `waitFor` with conditions)\n- Leave containers running after test failures\n- Commit test screenshots to git\n\n## Commands\n\n```bash\nbun run build                # Build frontend (REQUIRED first!)\nbun run e2e-test:variant console  # OSS E2E\nbun run e2e-test:variant console-enterprise  # Requires ENTERPRISE_BACKEND_DIR\nbun run e2e-test:variant console --watch  # Rstest watcher\nbun run e2e-test:variant console topics/topic-creation.spec.ts\nbun run e2e-test  # All four variants, including enterprise\n```\n\n## Test Architecture\n\n**OSS Mode (`bun run e2e-test:variant console`):** Redpanda + Backend + OwlShop containers\n**Enterprise Mode (`bun run e2e-test:variant console-enterprise`):** Same + RBAC, SSO (requires `console-enterprise` repo)\n\nFile location: `tests/test-variant-<variant>/<feature>/*.spec.ts`.\nImport `test`/`expect` from `tests/shared/e2e.ts` or the variant fixtures.\nSee [runner contracts](../../../tests/README.md).\n\n## Selector Priority\n\n1. `getByRole()` - Best for accessibility\n2. `getByLabel()` - For form inputs\n3. `getByText()` - For content verification\n4. `getByTestId()` - When semantic selectors aren't clear\n5. CSS selectors - Avoid if possible\n\n## Test ID Naming\n\n- kebab-case: `data-testid=\"feature-action-element\"`\n- Specific: include feature name + action + element type\n- Dynamic: `data-testid={\\`item-delete-\\${id}\\`}`\n\n## References\n\n- [Container Setup](references/container-setup.md) — Testcontainer lifecycle, configs, CI setup\n- [Test Patterns](references/test-patterns.md) — Multi-step workflows, forms, tables, API testing\n- [Failure Analysis](references/failure-analysis.md) — Error patterns and CLI debugging\n\n## Output\n\nAfter completing work:\n1. Confirm frontend build succeeded\n2. Verify all E2E tests pass\n3. Note any new test IDs added to components\n4. Mention cleanup of test containers\n\n<!-- File: frontend/.claude/skills/form-refactorer/references/common-patterns.md (Tokens: ~2843 | Category: anthropic-skill) -->\n# Common Form Patterns\n\nThis document provides patterns for common form scenarios using Field components.\n\n## Dynamic Fields\n\nForms with conditionally rendered fields based on user input.\n\n```tsx\nimport { useForm, useWatch } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { z } from \"zod\";\nimport {\n  FieldSet,\n  FieldLegend,\n  FieldGroup,\n  Field,\n  FieldLabel,\n  FieldError\n} from \"components/redpanda-ui/components/field\";\nimport { Select } from \"components/redpanda-ui/components/select\";\nimport { Input } from \"components/redpanda-ui/components/input\";\n\nconst schema = z.object({\n  authType: z.enum([\"none\", \"basic\", \"token\"]),\n  username: z.string().optional(),\n  password: z.string().optional(),\n  token: z.string().optional(),\n}).refine((data) => {\n  if (data.authType === \"basic\") {\n    return !!data.username && !!data.password;\n  }\n  if (data.authType === \"token\") {\n    return !!data.token;\n  }\n  return true;\n}, {\n  message: \"Authentication fields are required\",\n});\n\nexport const AuthForm = () => {\n  const { register, control, formState: { errors } } = useForm({\n    resolver: zodResolver(schema),\n  });\n\n  const authType = useWatch({ control, name: \"authType\" });\n\n  return (\n    <FieldSet>\n      <FieldLegend>Authentication</FieldLegend>\n      <FieldGroup>\n        <Field data-invalid={!!errors.authType}>\n          <FieldLabel htmlFor=\"authType\">Authentication Type</FieldLabel>\n          <Select id=\"authType\" {...register(\"authType\")}>\n            <option value=\"none\">None</option>\n            <option value=\"basic\">Basic Auth</option>\n            <option value=\"token\">Token</option>\n          </Select>\n          {errors.authType && <FieldError>{errors.authType.message}</FieldError>}\n        </Field>\n\n        {authType === \"basic\" && (\n          <>\n            <Field data-invalid={!!errors.username}>\n              <FieldLabel htmlFor=\"username\">Username</FieldLabel>\n              <Input id=\"username\" {...register(\"username\")} aria-invalid={!!errors.username} />\n              {errors.username && <FieldError>{errors.username.message}</FieldError>}\n            </Field>\n\n            <Field data-invalid={!!errors.password}>\n              <FieldLabel htmlFor=\"password\">Password</FieldLabel>\n              <Input id=\"password\" type=\"password\" {...register(\"password\")} aria-invalid={!!errors.password} />\n              {errors.password && <FieldError>{errors.password.message}</FieldError>}\n            </Field>\n          </>\n        )}\n\n        {authType === \"token\" && (\n          <Field data-invalid={!!errors.token}>\n            <FieldLabel htmlFor=\"token\">API Token</FieldLabel>\n            <Input id=\"token\" {...register(\"token\")} aria-invalid={!!errors.token} />\n            {errors.token && <FieldError>{errors.token.message}</FieldError>}\n          </Field>\n        )}\n      </FieldGroup>\n    </FieldSet>\n  );\n};\n```\n\n## Field Arrays (Repeatable Fields)\n\nForms with dynamically added/removed field groups.\n\n```tsx\nimport { useForm, useFieldArray } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { z } from \"zod\";\nimport {\n  FieldSet,\n  FieldLegend,\n  FieldGroup,\n  Field,\n  FieldLabel,\n  FieldError,\n  FieldSeparator\n} from \"components/redpanda-ui/components/field\";\nimport { Input } from \"components/redpanda-ui/components/input\";\nimport { Button } from \"components/redpanda-ui/components/button\";\n\nconst schema = z.object({\n  tags: z.array(z.object({\n    key: z.string().min(1, \"Key is required\"),\n    value: z.string().min(1, \"Value is required\"),\n  })).min(1, \"At least one tag is required\"),\n});\n\nexport const TagsForm = () => {\n  const { register, control, formState: { errors } } = useForm({\n    resolver: zodResolver(schema),\n    defaultValues: {\n      tags: [{ key: \"\", value: \"\" }],\n    },\n  });\n\n  const { fields, append, remove } = useFieldArray({\n    control,\n    name: \"tags\",\n  });\n\n  return (\n    <FieldSet>\n      <FieldLegend>Tags</FieldLegend>\n      <FieldGroup>\n        {fields.map((field, index) => (\n          <div key={field.id}>\n            {index > 0 && <FieldSeparator />}\n            <FieldGroup>\n              <Field data-invalid={!!errors.tags?.[index]?.key}>\n                <FieldLabel htmlFor={`tags.${index}.key`}>Key</FieldLabel>\n                <Input\n                  id={`tags.${index}.key`}\n                  {...register(`tags.${index}.key`)}\n                  aria-invalid={!!errors.tags?.[index]?.key}\n                />\n                {errors.tags?.[index]?.key && (\n                  <FieldError>{errors.tags[index].key.message}</FieldError>\n                )}\n              </Field>\n\n              <Field data-invalid={!!errors.tags?.[index]?.value}>\n                <FieldLabel htmlFor={`tags.${index}.value`}>Value</FieldLabel>\n                <Input\n                  id={`tags.${index}.value`}\n                  {...register(`tags.${index}.value`)}\n                  aria-invalid={!!errors.tags?.[index]?.value}\n                />\n                {errors.tags?.[index]?.value && (\n                  <FieldError>{errors.tags[index].value.message}</FieldError>\n                )}\n              </Field>\n\n              {fields.length > 1 && (\n                <Button\n                  type=\"button\"\n                  variant=\"destructive\"\n                  onClick={() => remove(index)}\n                >\n                  Remove Tag\n                </Button>\n              )}\n            </FieldGroup>\n          </div>\n        ))}\n\n        <Button\n          type=\"button\"\n          variant=\"secondary\"\n          onClick={() => append({ key: \"\", value: \"\" })}\n        >\n          Add Tag\n        </Button>\n      </FieldGroup>\n    </FieldSet>\n  );\n};\n```\n\n## Multi-Section Forms\n\nForms organized into multiple logical sections.\n\n```tsx\nimport {\n  FieldSet,\n  FieldLegend,\n  FieldDescription,\n  FieldGroup,\n  Field,\n  FieldLabel,\n  FieldSeparator,\n  FieldError\n} from \"components/redpanda-ui/components/field\";\nimport { Input } from \"components/redpanda-ui/components/input\";\nimport { Textarea } from \"components/redpanda-ui/components/textarea\";\nimport { Switch } from \"components/redpanda-ui/components/switch\";\n\nexport const ProfileSettingsForm = () => {\n  const { register, formState: { errors } } = useForm({\n    resolver: zodResolver(schema),\n  });\n\n  return (\n    <form>\n      {/* Personal Information Section */}\n      <FieldSet>\n        <FieldLegend>Personal Information</FieldLegend>\n        <FieldDescription>\n          This information will be displayed on your public profile.\n        </FieldDescription>\n        <FieldGroup>\n          <Field data-invalid={!!errors.name}>\n            <FieldLabel htmlFor=\"name\">Full Name</FieldLabel>\n            <Input id=\"name\" {...register(\"name\")} aria-invalid={!!errors.name} />\n            {errors.name && <FieldError>{errors.name.message}</FieldError>}\n          </Field>\n\n          <Field data-invalid={!!errors.bio}>\n            <FieldLabel htmlFor=\"bio\">Bio</FieldLabel>\n            <Textarea id=\"bio\" {...register(\"bio\")} aria-invalid={!!errors.bio} />\n            {errors.bio && <FieldError>{errors.bio.message}</FieldError>}\n          </Field>\n        </FieldGroup>\n      </FieldSet>\n\n      <FieldSeparator />\n\n      {/* Notification Preferences Section */}\n      <FieldSet>\n        <FieldLegend>Notification Preferences</FieldLegend>\n        <FieldDescription>\n          Manage how you receive notifications.\n        </FieldDescription>\n        <FieldGroup>\n          <Field orientation=\"horizontal\">\n            <Switch id=\"emailNotifications\" {...register(\"emailNotifications\")} />\n            <FieldContent>\n              <FieldLabel htmlFor=\"emailNotifications\">Email Notifications</FieldLabel>\n              <FieldDescription>Receive notifications via email</FieldDescription>\n            </FieldContent>\n          </Field>\n\n          <Field orientation=\"horizontal\">\n            <Switch id=\"pushNotifications\" {...register(\"pushNotifications\")} />\n            <FieldContent>\n              <FieldLabel htmlFor=\"pushNotifications\">Push Notifications</FieldLabel>\n              <FieldDescription>Receive push notifications in browser</FieldDescription>\n            </FieldContent>\n          </Field>\n        </FieldGroup>\n      </FieldSet>\n    </form>\n  );\n};\n```\n\n## Read-Only Fields\n\nDisplay form fields in a read-only state.\n\n```tsx\nimport {\n  Field,\n  FieldLabel,\n  FieldDescription\n} from \"components/redpanda-ui/components/field\";\nimport { Input } from \"components/redpanda-ui/components/input\";\n\nexport const ServerDetailsForm = ({ server }) => {\n  return (\n    <FieldGroup>\n      <Field>\n        <FieldLabel htmlFor=\"id\">Server ID</FieldLabel>\n        <Input id=\"id\" value={server.id} disabled />\n        <FieldDescription>Auto-generated unique identifier</FieldDescription>\n      </Field>\n\n      <Field>\n        <FieldLabel htmlFor=\"createdAt\">Created At</FieldLabel>\n        <Input id=\"createdAt\" value={server.createdAt} disabled />\n      </Field>\n    </FieldGroup>\n  );\n};\n```\n\n## Inline Form Actions\n\nForms with actions positioned within the field group.\n\n```tsx\nimport {\n  FieldSet,\n  FieldLegend,\n  FieldGroup,\n  Field,\n  FieldLabel,\n  FieldError\n} from \"components/redpanda-ui/components/field\";\nimport { Input } from \"components/redpanda-ui/components/input\";\nimport { Button } from \"components/redpanda-ui/components/button\";\n\nexport const InlineEditForm = () => {\n  const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm();\n\n  return (\n    <form onSubmit={handleSubmit(onSubmit)}>\n      <FieldSet>\n        <FieldLegend>Quick Edit</FieldLegend>\n        <FieldGroup>\n          <Field data-invalid={!!errors.name}>\n            <FieldLabel htmlFor=\"name\">Name</FieldLabel>\n            <div className=\"flex gap-2\">\n              <Input\n                id=\"name\"\n                {...register(\"name\")}\n                aria-invalid={!!errors.name}\n                className=\"flex-1\"\n              />\n              <Button type=\"submit\" disabled={isSubmitting}>\n                Save\n              </Button>\n            </div>\n            {errors.name && <FieldError>{errors.name.message}</FieldError>}\n          </Field>\n        </FieldGroup>\n      </FieldSet>\n    </form>\n  );\n};\n```\n\n## Common Zod Patterns\n\n```tsx\n// String validations\nz.string().min(1, \"Required\")\nz.string().email(\"Invalid email\")\nz.string().url(\"Invalid URL\")\nz.string().regex(/^[A-Z]/, \"Must start with capital\")\n\n// Number validations\nz.number().min(0, \"Must be positive\")\nz.number().max(100, \"Max 100\")\nz.number().int(\"Must be integer\")\n\n// Optional fields\nz.string().optional()\nz.string().nullable()\n\n// Arrays\nz.array(z.string()).min(1, \"At least one required\")\n\n// Objects\nz.object({\n  nested: z.string(),\n})\n\n// Conditional validation\nz.object({\n  type: z.enum([\"a\", \"b\"]),\n  value: z.string(),\n}).refine((data) => {\n  if (data.type === \"a\") return data.value.length > 5;\n  return true;\n}, \"Value must be longer than 5 for type A\")\n```\n\n## Common Pitfalls\n\n### Pitfall 1: Missing htmlFor/id Association\n\n```tsx\n// WRONG:\n<Field>\n  <FieldLabel>Email</FieldLabel>\n  <Input name=\"email\" />\n</Field>\n\n// CORRECT:\n<Field>\n  <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n  <Input id=\"email\" name=\"email\" />\n</Field>\n```\n\n### Pitfall 2: Missing aria-invalid\n\n```tsx\n// WRONG:\n<Field data-invalid={!!errors.email}>\n  <Input {...register(\"email\")} />\n</Field>\n\n// CORRECT:\n<Field data-invalid={!!errors.email}>\n  <Input {...register(\"email\")} aria-invalid={!!errors.email} />\n</Field>\n```\n\n### Pitfall 3: Conditional FieldError Without Check\n\n```tsx\n// WRONG:\n<FieldError>{errors.email.message}</FieldError>\n\n// CORRECT:\n{errors.email && <FieldError>{errors.email.message}</FieldError>}\n```\n\n### Pitfall 4: Using Legacy Form Components\n\n```tsx\n// WRONG:\nimport { Form } from \"components/redpanda-ui/components/form\";\n\n// CORRECT:\nimport { Field, FieldLabel } from \"components/redpanda-ui/components/field\";\n```\n\n### Pitfall 5: Forgetting FieldContent for Horizontal Fields\n\n```tsx\n// WRONG:\n<Field orientation=\"horizontal\">\n  <Switch id=\"notifications\" />\n  <FieldLabel htmlFor=\"notifications\">Enable</FieldLabel>\n  <FieldDescription>Receive updates</FieldDescription>\n</Field>\n\n// CORRECT:\n<Field orientation=\"horizontal\">\n  <Switch id=\"notifications\" />\n  <FieldContent>\n    <FieldLabel htmlFor=\"notifications\">Enable</FieldLabel>\n    <FieldDescription>Receive updates</FieldDescription>\n  </FieldContent>\n</Field>\n```\n\n<!-- File: frontend/.claude/skills/form-refactorer/SKILL.md (Tokens: ~677 | Category: anthropic-skill) -->\n---\nname: form-refactorer\ndescription: Refactor legacy forms to use modern Redpanda UI Registry Field components with react-hook-form and Zod validation. Use when user requests: (1) Form refactoring or modernization, (2) Updating forms to use Field components, (3) Migrating from legacy form patterns, (4) Implementing forms with react-hook-form and Zod validation.\n---\n\n# Form Refactorer\n\nRefactor legacy forms to modern Redpanda UI Registry Field components with react-hook-form and Zod validation.\n\n## Core Requirements\n\n### MUST USE (Modern Pattern)\n\n- Field component family from `components/redpanda-ui/components/field`\n- react-hook-form for form state management\n- Zod for validation schemas via `zodResolver`\n\n### FORBIDDEN (Legacy Patterns)\n\nSee [no-legacy](../code-standards/rules/no-legacy.md) for prohibited patterns. Also never import `Form` from `components/redpanda-ui/components/form`.\n\n## Component Hierarchy\n\n```\nFieldSet              - Semantic fieldset container for related fields\n├─ FieldLegend        - Title for the fieldset\n├─ FieldDescription   - Description for entire fieldset\n└─ FieldGroup         - Layout wrapper that stacks fields\n   ├─ Field           - Individual field wrapper\n   │  ├─ FieldLabel       - Label for input (use htmlFor)\n   │  ├─ Input/Select/... - Form control\n   │  ├─ FieldDescription - Helper text\n   │  └─ FieldError       - Error message\n   ├─ FieldSeparator  - Visual divider between fields\n   └─ Field           - Another field\n```\n\n## Key Patterns\n\n**Vertical field:**\n```tsx\n<Field data-invalid={!!errors.fieldName}>\n  <FieldLabel htmlFor=\"fieldName\">Label</FieldLabel>\n  <Input id=\"fieldName\" {...register(\"fieldName\")} aria-invalid={!!errors.fieldName} />\n  {errors.fieldName && <FieldError>{errors.fieldName.message}</FieldError>}\n</Field>\n```\n\n**Horizontal field (switch/checkbox):**\n```tsx\n<Field orientation=\"horizontal\">\n  <Switch id=\"toggle\" {...register(\"toggle\")} />\n  <FieldContent>\n    <FieldLabel htmlFor=\"toggle\">Enable Feature</FieldLabel>\n    <FieldDescription>Description</FieldDescription>\n  </FieldContent>\n</Field>\n```\n\n## Checklist\n\nBefore completing refactoring, verify:\n\n- [ ] All legacy form imports removed (the legacy `Form` component)\n- [ ] Field components used for all fields\n- [ ] FieldLabel has `htmlFor` matching Input `id`\n- [ ] Errors show `data-invalid` on Field and `aria-invalid` on Input\n- [ ] Yup replaced with Zod + zodResolver\n- [ ] FieldSet + FieldLegend used for semantic grouping\n- [ ] FieldError conditionally rendered: `{errors.field && <FieldError>...`\n- [ ] Form submission handled via react-hook-form `handleSubmit`\n\n## References\n\n- [Common Patterns](references/common-patterns.md) — Dynamic fields, field arrays, multi-section forms, pitfalls, Zod patterns\n- [Field Component Docs](https://redpanda-ui-registry.netlify.app/docs/field)\n\n<!-- File: frontend/.claude/skills/react-best-practices/rules/advanced-event-handler-refs.md (Tokens: ~337 | Category: anthropic-skill) -->\n---\ntitle: Store Event Handlers in Refs\nimpact: LOW\nimpactDescription: stable subscriptions\ntags: advanced, hooks, refs, event-handlers, optimization\n---\n\n## Store Event Handlers in Refs\n\nStore callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.\n\n**Incorrect (re-subscribes on every render):**\n\n```tsx\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  useEffect(() => {\n    window.addEventListener(event, handler)\n    return () => window.removeEventListener(event, handler)\n  }, [event, handler])\n}\n```\n\n**Correct (stable subscription):**\n\n```tsx\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  const handlerRef = useRef(handler)\n  useEffect(() => {\n    handlerRef.current = handler\n  }, [handler])\n\n  useEffect(() => {\n    const listener = (e) => handlerRef.current(e)\n    window.addEventListener(event, listener)\n    return () => window.removeEventListener(event, listener)\n  }, [event])\n}\n```\n\n**Alternative: use `useEffectEvent` if you're on latest React:**\n\n```tsx\nimport { useEffectEvent } from 'react'\n\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  const onEvent = useEffectEvent(handler)\n\n  useEffect(() => {\n    window.addEventListener(event, onEvent)\n    return () => window.removeEventListener(event, onEvent)\n  }, [event])\n}\n```\n\n`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.\n\n<!-- File: frontend/.claude/skills/react-best-practices/rules/advanced-use-latest.md (Tokens: ~295 | Category: anthropic-skill) -->\n---\ntitle: useLatest for Stable Callback Refs\nimpact: LOW\nimpactDescription: prevents effect re-runs\ntags: advanced, hooks, useLatest, refs, optimization\n---\n\n## useLatest for Stable Callback Refs\n\nAccess latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.\n\n**Implementation:**\n\n```typescript\nfunction useLatest<T>(value: T) {\n  const ref = useRef(value)\n  useLayoutEffect(() => {\n    ref.current = value\n  }, [value])\n  return ref\n}\n```\n\n**Incorrect (effect re-runs on every callback change):**\n\n```tsx\nfunction SearchInput({ onSearch }: { onSearch: (q: string) => void }) {\n  const [query, setQuery] = useState('')\n\n  useEffect(() => {\n    const timeout = setTimeout(() => onSearch(query), 300)\n    return () => clearTimeout(timeout)\n  }, [query, onSearch])\n}\n```\n\n**Correct (stable effect, fresh callback):**\n\n```tsx\nfunction SearchInput({ onSearch }: { onSearch: (q: string) => void }) {\n  const [query, setQuery] = useState('')\n  const onSearchRef = useLatest(onSearch)\n\n  useEffect(() => {\n    const timeout = setTimeout(() => onSearchRef.current(query), 300)\n    return () => clearTimeout(timeout)\n  }, [query])\n}\n```\n\n<!-- File: frontend/.claude/skills/react-best-practices/rules/async-defer-await.md (Tokens: ~437 | Category: anthropic-skill) -->\n---\ntitle: Defer Await Until Needed\nimpact: HIGH\nimpactDescription: avoids blocking unused code paths\ntags: async, await, conditional, optimization\n---\n\n## Defer Await Until Needed\n\nMove `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.\n\n**Incorrect (blocks both branches):**\n\n```typescript\nasync function handleRequest(userId: string, skipProcessing: boolean) {\n  const userData = await fetchUserData(userId)\n  \n  if (skipProcessing) {\n    // Returns immediately but still waited for userData\n    return { skipped: true }\n  }\n  \n  // Only this branch uses userData\n  return processUserData(userData)\n}\n```\n\n**Correct (only blocks when needed):**\n\n```typescript\nasync function handleRequest(userId: string, skipProcessing: boolean) {\n  if (skipProcessing) {\n    // Returns immediately without waiting\n    return { skipped: true }\n  }\n  \n  // Fetch only when needed\n  const userData = await fetchUserData(userId)\n  return processUserData(userData)\n}\n```\n\n**Another example (early return optimization):**\n\n```typescript\n// Incorrect: always fetches permissions\nasync function updateResource(resourceId: string, userId: string) {\n  const permissions = await fetchPermissions(userId)\n  const resource = await getResource(resourceId)\n  \n  if (!resource) {\n    return { error: 'Not found' }\n  }\n  \n  if (!permissions.canEdit) {\n    return { error: 'Forbidden' }\n  }\n  \n  return await updateResourceData(resource, permissions)\n}\n\n// Correct: fetches only when needed\nasync function updateResource(resourceId: string, userId: string) {\n  const resource = await getResource(resourceId)\n  \n  if (!resource) {\n    return { error: 'Not found' }\n  }\n  \n  const permissions = await fetchPermissions(userId)\n  \n  if (!permissions.canEdit) {\n    return { error: 'Forbidden' }\n  }\n  \n  return await updateResourceData(resource, permissions)\n}\n```\n\nThis optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.\n\n<!-- File: frontend/.claude/skills/react-best-practices/rules/async-dependencies.md (Tokens: ~213 | Category: anthropic-skill) -->\n---\ntitle: Dependency-Based Parallelization\nimpact: CRITICAL\nimpactDescription: 2-10× improvement\ntags: async, parallelization, dependencies, better-all\n---\n\n## Dependency-Based Parallelization\n\nFor operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.\n\n**Incorrect (profile waits for config unnecessarily):**\n\n```typescript\nconst [user, config] = await Promise.all([\n  fetchUser(),\n  fetchConfig()\n])\nconst profile = await fetchProfile(user.id)\n```\n\n**Correct (config and profile run in parallel):**\n\n```typescript\nimport { all } from 'better-all'\n\nconst { user, config, profile } = await all({\n  async user() { return fetchUser() },\n  async config() { return fetchConfig() },\n  async profile() {\n    return fetchProfile((await this.$.user).id)\n  }\n})\n```\n\nReference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)\n\n<!-- File: frontend/.claude/skills/react-best-practices/rules/async-suspense-boundaries.md (Tokens: ~591 | Category: anthropic-skill) -->\n---\ntitle: Strategic Suspense Boundaries\nimpact: HIGH\nimpactDescription: faster initial paint\ntags: async, suspense, streaming, layout-shift\n---\n\n## Strategic Suspense Boundaries\n\nInstead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.\n\n**Incorrect (wrapper blocked by data fetching):**\n\n```tsx\nasync function Page() {\n  const data = await fetchData() // Blocks entire page\n  \n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <div>\n        <DataDisplay data={data} />\n      </div>\n      <div>Footer</div>\n    </div>\n  )\n}\n```\n\nThe entire layout waits for data even though only the middle section needs it.\n\n**Correct (wrapper shows immediately, data streams in):**\n\n```tsx\nfunction Page() {\n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <div>\n        <Suspense fallback={<Skeleton />}>\n          <DataDisplay />\n        </Suspense>\n      </div>\n      <div>Footer</div>\n    </div>\n  )\n}\n\nasync function DataDisplay() {\n  const data = await fetchData() // Only blocks this component\n  return <div>{data.content}</div>\n}\n```\n\nSidebar, Header, and Footer render immediately. Only DataDisplay waits for data.\n\n**Alternative (share promise across components):**\n\n```tsx\nfunction Page() {\n  // Start fetch immediately, but don't await\n  const dataPromise = fetchData()\n  \n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <Suspense fallback={<Skeleton />}>\n        <DataDisplay dataPromise={dataPromise} />\n        <DataSummary dataPromise={dataPromise} />\n      </Suspense>\n      <div>Footer</div>\n    </div>\n  )\n}\n\nfunction DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {\n  const data = use(dataPromise) // Unwraps the promise\n  return <div>{data.content}</div>\n}\n\nfunction DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {\n  const data = use(dataPromise) // Reuses the same promise\n  return <div>{data.summary}</div>\n}\n```\n\nBoth components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.\n\n**When NOT to use this pattern:**\n\n- Critical data needed for layout decisions (affects positioning)\n- SEO-critical content above the fold\n- Small, fast queries where suspense overhead isn't worth it\n- When you want to avoid layout shift (loading → content jump)\n\n**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.\n\n<!-- File: frontend/.claude/skills/react-best-practices/rules/bundle-barrel-imports.md (Tokens: ~499 | Category: anthropic-skill) -->\n---\ntitle: Avoid Barrel File Imports\nimpact: CRITICAL\nimpactDescription: 200-800ms import cost, slow builds\ntags: bundle, imports, tree-shaking, barrel-files, performance\n---\n\n## Avoid Barrel File Imports\n\nImport directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).\n\nPopular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.\n\n**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.\n\n**Incorrect (imports entire library):**\n\n```tsx\nimport { Check, X, Menu } from 'lucide-react'\n// Loads 1,583 modules, takes ~2.8s extra in dev\n// Runtime cost: 200-800ms on every cold start\n\nimport { Button, TextField } from '@mui/material'\n// Loads 2,225 modules, takes ~4.2s extra in dev\n```\n\n**Correct (imports only what you need):**\n\n```tsx\nimport Check from 'lucide-react/dist/esm/icons/check'\nimport X from 'lucide-react/dist/esm/icons/x'\nimport Menu from 'lucide-react/dist/esm/icons/menu'\n// Loads only 3 modules (~2KB vs ~1MB)\n\nimport Button from '@mui/material/Button'\nimport TextField from '@mui/material/TextField'\n// Loads only what you use\n```\n\nDirect imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.\n\nLibraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.\n\nReference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)\n\n<!-- File: frontend/.claude/skills/react-best-practices/rules/bundle-code-splitting.md (Tokens: ~446 | Category: anthropic-skill) -->\n---\ntitle: Code Splitting and Lazy Loading\nimpact: CRITICAL\nimpactDescription: directly affects TTI and LCP\ntags: bundle, dynamic-import, code-splitting, react-lazy, conditional-loading\n---\n\n## Code Splitting and Lazy Loading\n\nUse `React.lazy` with `Suspense` to lazy-load large components not needed on initial render. Load large data modules conditionally. Defer non-critical third-party libraries.\n\n### Dynamic Imports for Heavy Components\n\n**Incorrect (Monaco bundles with main chunk ~300KB):**\n\n```tsx\nimport { MonacoEditor } from './monaco-editor'\n```\n\n**Correct (Monaco loads on demand):**\n\n```tsx\nimport { lazy, Suspense } from 'react'\n\nconst MonacoEditor = lazy(() =>\n  import('./monaco-editor').then(m => ({ default: m.MonacoEditor }))\n)\n\nfunction CodePanel({ code }: { code: string }) {\n  return (\n    <Suspense fallback={<div>Loading editor...</div>}>\n      <MonacoEditor value={code} />\n    </Suspense>\n  )\n}\n```\n\n### Conditional Module Loading\n\nLoad large data or modules only when a feature is activated:\n\n```tsx\nfunction AnimationPlayer({ enabled, setEnabled }: Props) {\n  const [frames, setFrames] = useState<Frame[] | null>(null)\n\n  useEffect(() => {\n    if (enabled && !frames) {\n      import('./animation-frames.js')\n        .then(mod => setFrames(mod.frames))\n        .catch(() => setEnabled(false))\n    }\n  }, [enabled, frames, setEnabled])\n\n  if (!frames) return <Skeleton />\n  return <Canvas frames={frames} />\n}\n```\n\n### Defer Non-Critical Third-Party Libraries\n\nAnalytics, logging, and error tracking don't block user interaction:\n\n```tsx\nconst Analytics = lazy(() =>\n  import('@some-vendor/analytics').then(m => ({ default: m.Analytics }))\n)\n\nexport function App({ children }) {\n  return (\n    <div>\n      {children}\n      <Suspense fallback={null}>\n        <Analytics />\n      </Suspense>\n    </div>\n  )\n}\n```\n\n<!-- ... +59 more modular skills available via /api/skills/redpanda-data/console?path={path} -->\n</yakaai_skills>"}