Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# cozystack-ui

Cozystack Marketplace + Console — a **pure SPA that talks directly to the
Kubernetes API**. No BFF, no backend, no codegen. UI entities are discovered
at runtime from `ApplicationDefinition` CRDs in the cluster.

## Development model

This UI is **developed entirely by Claude**. There is no human author writing
features in parallel. Every file in this repo was produced through a Claude
session and every change should be reviewable as a small, self-contained PR.
Implications:

- Keep modules small and obvious. The next Claude session has no tribal
knowledge — only what's in code, tests, and this file.
- Prefer convention over abstraction. Three similar widgets are better than a
premature factory.
- If you're tempted to write a comment explaining *what* the code does, rename
things instead. Only comment the non-obvious *why* (constraints, k8s quirks,
workarounds) — and keep it to one short line.
- Don't leave half-finished scaffolding behind. If you start a refactor and
bail, revert it.

## Stack

- **pnpm workspace** — `apps/console` is the SPA; `packages/{k8s-client,ui,types}` are workspace deps.
- **React 19 + Vite 8 + TypeScript ~6.0** with `module: esnext`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The versions for Vite (8) and TypeScript (6.0) are incorrect as these versions have not been released. This may cause the LLM to assume the availability of non-existent features or APIs. Consider updating these to the actual versions used in the project (e.g., Vite 6 and TypeScript 5.x).

`moduleResolution: bundler`, `verbatimModuleSyntax`, `allowImportingTsExtensions`,
`erasableSyntaxOnly`, `noUnusedLocals`, `noUnusedParameters`.
- **Tailwind 4** via `@tailwindcss/vite` + **Base UI** (`@base-ui/react`) for primitives.
- **React Query** for caching; custom watch layer streams chunked JSON events.
- **RJSF** (`@rjsf/core` + `validator-ajv8`) for application config forms.
- **Vitest + jsdom + @testing-library/react** for tests.
- **Monaco** for YAML view, **noVNC** for the VM console tab.

## Dev loop

```sh
pnpm install
kubectl proxy --port 8001 # required — terminates TLS, proxies /api + /apis
pnpm dev # http://localhost:3001
pnpm typecheck # tsc --noEmit across the workspace
pnpm test # vitest run
pnpm lint # eslint
pnpm build # tsc check + vite build into apps/console/dist
```

The Vite dev server proxies `/api`, `/apis`, and `/k8s` (VNC WebSocket prefix)
to `kubectl proxy`. In production, nginx (see `Containerfile`) proxies the
same paths to `kubernetes.default.svc` using the pod's service-account token.

## Code style

- **No semicolons.** Match the surrounding code — every file in the tree
follows this. Don't reformat existing files.
- **Always import with explicit `.ts` / `.tsx` extensions** — required by
`allowImportingTsExtensions` and the way packages re-export.
- **`import type { ... }` for type-only imports** — required by `verbatimModuleSyntax`.
- **Path aliases**: `@/` → `apps/console/src/`, plus the workspace deps
`@cozystack/{k8s-client,ui,types}`. Don't reach into `../../packages/...`.
- **No `any`.** Use `unknown` and narrow, or a precise local interface.
`as any` casts in older files are debt to pay down, not a pattern to extend.
- **No new top-level deps without a reason in the PR description.** This is a
static SPA that ships to every Cozystack cluster — bundle size matters.

## Architecture rules

1. **Talk to the Kubernetes API directly.** Use `@cozystack/k8s-client`
(`useK8sList`, `useK8sGet`, `useK8sCreate`, `useK8sUpdate`, `useK8sDelete`).
Don't add a backend, server route, or proxy of your own.
2. **Discover, don't hardcode.** The marketplace, sidebar, detail pages, and
forms are all driven from `ApplicationDefinition` resources
(`cozystack.io/v1alpha1`). Adding a new application kind to Cozystack
should require zero UI changes — if it doesn't, fix the generic path
instead of adding a special case.
3. **`useK8sList` already does watches.** It seeds with a LIST then upgrades
to a chunked-encoding WATCH against the same `resourceVersion`. Don't
poll. Don't add `refetchInterval`. If you need a one-shot, pass
`{ watch: false }`.
4. **Tenant scoping.** Most resources live in `tenant-<name>` namespaces.
Pull the active tenant from `useTenantContext()` — never read the
namespace from a URL param or guess it.
5. **Auth.** In production the SPA sits behind oauth2-proxy. The client
relies on cookies forwarded by nginx, and `/oauth2/userinfo` returns the
logged-in user. There is no token handling in the SPA itself.

## Forms (RJSF) pipeline

Every application's configure form is built from
`ApplicationDefinition.spec.application.openAPISchema` (a JSON-encoded OpenAPI
schema). The pipeline in `apps/console/src/components/SchemaForm.tsx`:

1. `sanitizeSchema` strips Kubernetes-specific extensions
(`x-kubernetes-int-or-string`, `x-kubernetes-preserve-unknown-fields`) and
renames `"Chart Values"` → `"Parameters"`.
2. `keysOrderToUiSchema` reads `spec.dashboard.keysOrder` and emits per-level
`ui:order` arrays.
3. A chain of `addXxxWidgets(schema, uiSchema)` walks the schema and binds
widgets by **field name** convention:
- `storageClass` → `StorageClassWidget`
- `backupClassName` → `BackupClassWidget`
- `disks[].name` → `VMDiskWidget`
- object with `additionalProperties: <schema>` → `AdditionalPropertiesField`
- credential-shaped fields (`password`, `*token`, `*accessKey`, …) →
`SensitiveStringWidget` — see `lib/sensitive-fields.ts` and its tests for
the exact matching rules.
4. Defaults are emitted to the parent once per schema via `getDefaultFormState`
so the first submit always carries a populated spec.

When you add a new widget binding:

- Add it to the chain in `SchemaForm.tsx` in a deterministic order.
- Walk `properties` *and* `items` for arrays. Do **not** walk
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The guide states that widget binding logic should walk both properties and items for arrays. However, several helper functions in apps/console/src/components/SchemaForm.tsx (such as addStorageClassWidgets, addBackupClassWidgets, and addAdditionalPropertiesWidgets) currently only walk properties. This inconsistency should be resolved to ensure the guide accurately reflects the required implementation pattern.

`oneOf`/`anyOf`/`allOf` unless a real chart needs it — there's a
"pin broken behaviour" test that documents this gap intentionally.
- Don't mutate the input `uiSchema`. Return a new object. There are tests
asserting this.

## Project layout

```
apps/console/
src/
App.tsx, main.tsx # entry + routing
routes/ # one file per top-level page
detail/ # ApplicationDetailPage + tabs (Overview, Workloads, …)
components/ # page-level components, form widgets, command palette
lib/ # app-definitions, tenant-context, sensitive-fields, …
hooks/
test/setup.ts # vitest + jest-dom + manual RTL cleanup
packages/
k8s-client/ # K8sClient (list/get/create/update/patch/delete/watch) + React Query hooks
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The description for packages/k8s-client/ mentions patch support, but the useK8sPatch hook is missing from packages/k8s-client/src/hooks.ts. If patching is a supported operation, the corresponding hook should be implemented or the documentation updated to reflect the available hooks.

ui/ # AppShell, Sidebar, Header, Button, StatusBadge, Spinner, Dropdown, Section
types/ # ApplicationDefinition, ApplicationInstance, Tenant, TenantNamespace, group/version constants
Containerfile # multi-stage build → nginx-unprivileged on :8080
.github/workflows/ # test.yaml (typecheck + vitest), build.yaml (multi-arch image to ghcr)
```
Comment on lines +121 to +137
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language to the fenced code block to satisfy markdownlint.

Line 121 opens a fenced block without a language, which triggers MD040. Add a language token (for example text) to avoid lint noise/failures.

Proposed fix
-```
+```text
 apps/console/
   src/
     App.tsx, main.tsx              # entry + routing
@@
 .github/workflows/                  # test.yaml (typecheck + vitest), build.yaml (multi-arch image to ghcr)
-```
+```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
apps/console/
src/
App.tsx, main.tsx # entry + routing
routes/ # one file per top-level page
detail/ # ApplicationDetailPage + tabs (Overview, Workloads, …)
components/ # page-level components, form widgets, command palette
lib/ # app-definitions, tenant-context, sensitive-fields, …
hooks/
test/setup.ts # vitest + jest-dom + manual RTL cleanup
packages/
k8s-client/ # K8sClient (list/get/create/update/patch/delete/watch) + React Query hooks
ui/ # AppShell, Sidebar, Header, Button, StatusBadge, Spinner, Dropdown, Section
types/ # ApplicationDefinition, ApplicationInstance, Tenant, TenantNamespace, group/version constants
Containerfile # multi-stage build → nginx-unprivileged on :8080
.github/workflows/ # test.yaml (typecheck + vitest), build.yaml (multi-arch image to ghcr)
```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 121-121: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 121 - 137, Add a language token to the fenced code
block in CLAUDE.md (the multi-line listing block that begins with ``` and shows
the apps/console/ structure) so markdownlint rule MD040 stops flagging it;
change the opening fence from ``` to ```text (or another appropriate language)
and leave the closing ``` unchanged.


## Testing

- Tests live next to the code they cover (`SensitiveStringWidget.test.tsx`,
`sensitive-fields.test.ts`). Co-locate; don't centralise.
- `apps/console/test/setup.ts` wires `@testing-library/jest-dom` and registers
`afterEach(cleanup)` manually — vitest is **not** run with `globals: true`,
so `expect`, `describe`, etc. must be imported explicitly.
- When you fix a subtle widget bug, write the test first. The
`addSensitiveStringWidgets` suite is the model: small focused cases, one
invariant per `it`, and a "pin broken behaviour" group for known gaps.

## CI

`test.yaml` runs `pnpm typecheck` + `pnpm test` on every push and PR.
`build.yaml` builds and pushes a multi-arch image to
`ghcr.io/<repo>` on `main` and `v*` tags. Don't add other workflows without a
clear reason — keep CI fast.

## What goes in a good PR here

- One feature or one fix per branch. Small enough that a human can review it
in a few minutes without context.
- A title that follows the conventional-commit style already in use
(`feat(console): …`, `fix(forms): …`, `fix(external-ips): …`).
- If you touched form logic, add or update tests.
- If you added a new application kind to the sidebar/marketplace, double-check
that the generic path handles it instead of special-casing.
- Don't bundle drive-by reformatting with a behavioural change.
Loading