# Tech Stack Decisions — Unit 3: frontend-cms-page ## Existing stack — no changes | Layer | Technology | Notes | |-------|-----------|-------| | UI components | Shadcn/UI | `Dialog`, `Table`, `Select`, `Badge`, `DropdownMenu` — all already present | | Styling | Tailwind CSS | `opacity-50` for Inactive rows (BR-FE-04) | | Data fetching | TanStack Query v5 | `useQuery` + `useMutation` pattern already used by `useUsers` | | Form state | react-hook-form + zodResolver | Used in `InviteUserDialog`, `LoginPage`, `SetupPage` | | Schema validation | Zod | `z.string().url()` for URL field (NFR-FE-13) | | Routing | TanStack Router | `/cms` route already exists; `RoleGuard` already wired | | i18n | react-i18next | New `cms.*` keys added to EN + NL locales | | Toasts | Sonner | `toast.success()` / `toast.error()` — already used throughout | | Testing | Vitest + React Testing Library + MSW | All already configured | | HTTP client | Custom `api-client` (`NetworkError`, `ProblemDetailsError`) | Same error types used across all existing hooks | ## Decisions made in this unit ### TSD-01 — URL validation: `z.string().url()` (strict) **Decision**: Use Zod's built-in `z.string().url()` for the URL field. **Rationale**: Q1: A. Users must enter a fully-qualified URL with protocol. The backend validates anyway, but client-side strict validation stops obviously malformed input early and improves error feedback. **Implication**: Users entering `192.168.1.5:8080` will see a validation error. They must enter `http://192.168.1.5:8080`. ### TSD-02 — Test scope: hooks + page + component tests **Decision**: Three test layers: 1. Hook unit tests (`useCmsInstances.test.ts`, `useAddCmsInstance.test.ts`, `useUpdateCmsInstanceStatus.test.ts`) using `renderHook` + MSW server 2. Page integration test (`CmsPage.test.tsx`) covering load, empty state, add flow, status flow 3. Component tests (`AddCmsInstanceDialog.test.tsx`, `SetStatusDialog.test.tsx`) for error states and conditional logic **Rationale**: Q2: C. Matches `InviteUserDialog.test.tsx` precedent. Dialog component tests are necessary to cover `FormErrorBanner` on HTTP 400 and the `DisableMessage` conditional show/hide (BR-FE-08) without a full page mount. ### TSD-03 — ApiKey field: `PasswordField` with show/hide toggle **Decision**: Reuse the existing `PasswordField` component (`src/components/ui/PasswordField.tsx`) for the `apiKey` input. **Rationale**: Q3: A. Hides the credential from shoulder-surfing while allowing the user to reveal it for verification. No new component needed — `PasswordField` already implements the show/hide pattern. ### TSD-04 — Toast content: differentiated on `slaveContactSuccess` **Decision**: Two distinct toast messages for `PUT /api/v1/CmsInstances/{id}/status`: - `slaveContactSuccess === true` → `t('cms.setStatus.successContactedToast')` = "Status updated — cliënt confirmed" - `slaveContactSuccess === false` → `t('cms.setStatus.successUnreachableToast')` = "Status saved — cliënt unreachable" **Rationale**: Q3 FD: B + user terminology preference (no "slave" in UI). Both cases are HTTP 200; the distinction is surfaced via the `UpdateStatusResult.slaveContactSuccess` field from the backend. ### TSD-05 — `FormErrorBanner` component name The project has both `FormBannerError` (`src/components/ui/FormBannerError.tsx`) and `FormErrorBanner` (`src/components/ui/FormErrorBanner.tsx`). The dialogs use `FormErrorBanner` (as used in `InviteUserDialog`). The `AddCmsInstanceDialog` and `SetStatusDialog` will import from `@/components/ui/FormErrorBanner`.