diff --git a/CLAUDE.md b/CLAUDE.md index 400fd54..d57a0eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,111 +1,149 @@ -You are an expert in TypeScript, Angular, and scalable web application development. You write functional, maintainable, performant, and accessible code following Angular and TypeScript best practices. +You are an expert in TypeScript, Angular, and PocketBase. You write functional, maintainable, performant, and accessible code following Angular and TypeScript best practices. -## Project Overview +## Project -Pchelpcrm is a CRM for handling service requests (orders) of a private service professional: computer help, home repairs, furniture assembly, plumbing, electrical work, etc. The domain is deliberately generic: any "client calls, a job gets done" business fits. Currently used personally by the author, so keep it simple. +Pchelpcrm is a multi-user CRM for private service professionals: clients, orders, tags, and a public link to an order. Angular 22 with SSR + PocketBase 0.40 + pnpm. The description, commands, and repository structure are in `README.md`. Keep solutions simple. -Domain entities: +## Before you start -- **User** — the owner of the personal account who works in the CRM (a PocketBase auth record). Everything below belongs to a user and must be isolated per user (API rules). -- **Client** — a customer of the user. A separate entity, not a user. -- **Request** — a service request (order). Belongs to exactly one client; one client can have many requests. -- **Category** — a request category. Dynamic: created and edited by the user, never hardcoded in code or enums. +Read: -Auth: PocketBase auth collection, login by email + password. +1. `docs/architecture.md` — layers, DI, SSR, security, UI principles; +2. `docs/data-model.md` — collections, fields, access rules; +3. `docs/testing.md` — how tests are written; +4. the page `docs/guide/.md` for the part you are changing, if it already exists (table of contents — `docs/README.md`). -Stack: Angular (see rules below) + PocketBase (`pocketbase/pb_migrations/` holds the schema) + pnpm. Add other libraries (e.g. a UI component library) only when really needed. +## How to work -Delivery plan: after the first release the app ships as ONE Docker container (Angular app + PocketBase together, no docker-compose) deployable to a production server. Do not design anything that requires multiple containers. +- Develop with TDD: tests first, then the implementation. +- If a task contains a "Forbidden" section ("Запрещено"), obey it. +- If a solution diverges from `docs/architecture.md` or `docs/data-model.md`, stop and discuss the document change with the user. +- Before finishing, run `pnpm lint` and all existing test suites (`pnpm test`, `pnpm test:api`, `pnpm test:integration`, `pnpm e2e` — those already set up). +- Update the documentation in the same change (see the "Documentation" section below). -## TypeScript Best Practices +## Domain -- Use strict type checking +- An order in code is `Order`; the collection, folder, and routes are `orders`. +- Tags (`Tag`) are created by the user. They replace categories and sources: no separate categories/sources and no hardcoded lists or enums of tags in code. +- Everything except the user belongs to the user (the `owner` field). Isolation is enforced by PocketBase access rules, not by the frontend. +- Delivery is ONE Docker container (SSR server + PocketBase). Do not design anything that requires multiple containers. + +## Architecture + +- Layers inside a feature: `domain/`, `data/`, `ui/`, `testing/`. `domain/` does not import `@angular/*` or `pocketbase`; `ui/` does not import `pocketbase` or `data/`. PocketBase records never leave `data/`. +- Repositories and services that may get another implementation are described by an abstract class (which doubles as the DI token) and are registered in the feature's `provideXxx()`. +- A feature imports another feature only through its `index.ts`, with no cycles; feedback goes through extension points (`InjectionToken`). +- There is no separate store layer; a signal-based store only if several components share the same state. +- PocketBase errors are translated into domain errors from `shared/`. + +## SSR and data isolation + +- **Forbidden** to create PocketBase at module level or to keep user data in module variables, static fields, and caches outside DI. `new PocketBase(` only in the factory in `shared/pocketbase/`. +- Do not touch `window`/`document` without a platform check; use `DOCUMENT`, `isPlatformBrowser`, `afterNextRender`. +- The browser reaches PocketBase via `/pb/*`; the SSR server goes directly via `PB_URL` and does not proxy the API. +- Protected files (attachments) are not rendered on the server. + +## Project structure + +- Code is organized by feature, not by file type: `src/app//` (e.g. `src/app/clients/`), not global `components/`, `services/`, `pipes/` folders +- Everything a feature needs lives inside its folder, including routes and tests +- One concept per file: one component, directive, pipe, or service per file +- Files of the same entity sit together with the same base name (`order-list.ts`, `order-list.html`, `order-list.css`, `order-list.spec.ts`) +- File names in kebab-case. Do NOT add `.component` / `.service` suffixes (like the existing `app.ts`) +- Each feature has its own routes file (`.routes.ts`), and the feature is lazy-loaded from `app.routes.ts` +- Shared infrastructure goes in `src/app/shared/`, only when the code is really needed by two or more features. Do not create a `core/` folder + +## TypeScript + +- Strict type checking - Prefer type inference when the type is obvious -- Avoid the `any` type; use `unknown` when type is uncertain +- Avoid `any`; if the type is unknown, use `unknown` -## Angular Best Practices +## Angular - Always use standalone components over NgModules -- Must NOT set `standalone: true` inside Angular decorators. It's the default in Angular v20+. -- Do NOT set `changeDetection: ChangeDetectionStrategy.OnPush` explicitly. `OnPush` is the default in Angular v22+. -- Use signals for state management -- Implement lazy loading for feature routes -- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead +- Do NOT set `standalone: true` in Angular decorators — it is the default in Angular v20+. +- Do NOT set `changeDetection: ChangeDetectionStrategy.OnPush` explicitly — `OnPush` is the default in Angular v22+. +- The selector prefix for components and directives is `ksk` (`ksk-order-list`, `kskAutofocus`) +- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings in the `host` object of the `@Component` or `@Directive` decorator - Use `NgOptimizedImage` for all static images. - `NgOptimizedImage` does not work for inline base64 images. -## Accessibility Requirements - -- It MUST pass all AXE checks. -- It MUST follow all WCAG AA minimums, including focus management, color contrast, and ARIA attributes. - -## UI / Visual Design - -- All visual and frontend work (layout, styling, components' look, colors, typography, responsive behavior) MUST be done using the skills `ui-ux-pro-max` and `frontend-design` (`frontend-design:frontend-design`) -- Invoke both skills via the Skill tool before building or reshaping any UI -- Always get feedback from the real browser via the Playwright MCP (`mcp__playwright__*`): use it during UI development and to verify every visual/frontend change (snapshot, screenshot, console messages, responsive sizes via `browser_resize`) -- Skill recommendations must not violate the Accessibility Requirements and the Angular rules of this file; on conflict, this file wins - -## Components +### Components - Keep components small and focused on a single responsibility - Use `input()` and `output()` functions instead of decorators -- Use `model()` for two-way bound properties with `[(prop)]` syntax instead of pairing `input()` with `output()` -- Use `computed()` for derived state -- Use `linkedSignal()` for state derived from multiple reactive sources that must stay synchronized -- Prefer inline templates for small components -- Prefer Signal Forms (`@angular/forms/signals`) for new forms. They are stable in Angular v22+ and provide signal-based state, type-safe field access, and schema-based validation -- When not using Signal Forms, prefer Reactive forms instead of Template-driven ones -- Do NOT use `ngClass`, use `class` bindings instead -- Do NOT use `ngStyle`, use `style` bindings instead -- Do NOT import `CommonModule`, import only the directives and pipes the template uses, such as `AsyncPipe` or `DatePipe` -- When using external templates/styles, use paths relative to the component TS file. +- Use `model()` for two-way binding with the `[(prop)]` syntax instead of pairing `input()` with `output()` +- Prefer inline templates for small components; move large templates and styles to separate files +- In external templates/styles, use paths relative to the component's TS file. +- Do NOT import `CommonModule`; import only the directives and pipes the template uses, such as `AsyncPipe` or `DatePipe` -## State Management +### State -- Use signals for local component state -- Use `computed()` for derived state +- Signals for local component state +- `computed()` for derived state +- `linkedSignal()` for state derived from multiple reactive sources that must stay synchronized - Keep state transformations pure and predictable -- Do NOT use `mutate` on signals, use `update` or `set` instead +- Do NOT use `mutate` on signals; use `update` or `set` +- Load data asynchronously in components with `resource()` (SSR waits for it to complete) -## Templates +### Templates -- Keep templates simple and avoid complex logic -- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch` -- Use the async pipe to handle observables -- Do not assume globals like (`new Date()`) are available. +- Keep templates simple, without complex logic +- Native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch` +- Do NOT use `ngClass`, use `class` bindings; do NOT use `ngStyle`, use `style` bindings +- Use the async pipe for observables +- Do not assume globals (e.g. `new Date()`) are available in templates. +- User-provided text is never inserted via `innerHTML` -## Services +### Forms -- Design services around a single responsibility -- Use the `providedIn: 'root'` option for singleton services +- Prefer Signal Forms (`@angular/forms/signals`) for new forms. They are stable in Angular v22+ and provide signal-based state, type-safe field access, and schema-based validation +- If not Signal Forms, use Reactive forms, not Template-driven ones + +### Services + +- A service is responsible for a single task +- `providedIn: 'root'` for singletons - Prefer the `@Service` decorator over `@Injectable({providedIn: 'root'})` for new singleton services (Angular v22+) - Use the `inject()` function instead of constructor injection -## Routing +### Routing -- All routes are lazy loaded (`loadComponent` / `loadChildren`), including feature-internal ones -- All routes render as a plain client-side SPA (`RenderMode.Client` in `app.routes.server.ts`). Do NOT use SSR or prerender by default; enable them only for specific routes the user explicitly asks for +- All routes are lazy-loaded (`loadComponent` / `loadChildren`), including the internal routes of features +- `RenderMode.Server` in `app.routes.server.ts` only for `''` (home), `orders/:id` (order page), `p/:token` (public page). All other routes are `RenderMode.Client`. New SSR routes only at the user's explicit request +- Private routes are protected by `authGuard` -## Project Structure +## UI and accessibility -- Organize code by feature, not by file type: `src/app//` (e.g. `src/app/clients/`), not global `components/`, `services/`, `pipes/` folders -- Keep everything a feature needs inside its folder: components, services, models, routes and tests -- One concept per file: one component, directive, pipe or service per file -- Keep the files of one entity together in the same folder with the same base name (`order-list.ts`, `order-list.html`, `order-list.css`, `order-list.spec.ts`) -- Use kebab-case file names. Do NOT add `.component` / `.service` suffixes (matches the existing `app.ts`) -- Put unit tests next to the code they test (`*.spec.ts`) -- Each feature has its own routes file (`.routes.ts`) and is lazy loaded from `app.routes.ts` via `loadChildren` / `loadComponent` -- Do NOT create `core/` or `shared/` folders upfront; add shared code only when two or more features actually need it -- Features must not import from each other's internals; extract the common part to a shared location instead +- The app MUST pass all AXE checks and meet WCAG AA minimums, including focus management, color contrast, and ARIA attributes. +- All visual and frontend work (layout, styles, look of components, colors, typography, responsiveness) MUST be done using the skills `ui-ux-pro-max` and `frontend-design` (`frontend-design:frontend-design`). Invoke both skills via the Skill tool before building or reshaping any UI. +- Always get feedback from a real browser via the Playwright MCP (`mcp__playwright__*`): during UI development and to verify every visual change (snapshot, screenshot, console messages, responsive sizes via `browser_resize`). +- Skill recommendations must not violate the rules of this file; on conflict, this file wins. +- Plain CSS without preprocessors or CSS frameworks; colors, spacing, and sizes only via design tokens in `src/styles.css`. Dark theme via `prefers-color-scheme`. +- Minimal GPU load: animate only `transform` and `opacity`, respect `prefers-reduced-motion`; no `backdrop-filter`, `filter: blur()`, heavy or layered shadows, animated gradients, or permanent `will-change` (details — `docs/architecture.md`, section "Формы и UI"). +- The UI is in Russian only (no i18n framework); dates and amounts use the `ru-RU` locale, the currency is rubles. Mobile-first. -## Git Branches +## Tests -- Follow GitHub Flow: there is NO separate `develop` branch. `main` is the only long-lived branch -- Create every working branch directly from the latest `main`: features, fixes and release preparation alike -- Name branches by purpose in kebab-case: `feat/`, `fix/`, `release/` -- Do NOT create branches on your own: the user creates a branch (named as above) when a task is large (a feature, many related changes). Otherwise work in the current branch -- Small point corrections (config tweaks, adding a dependency, minor fixes) need no separate branch: they go straight to the current branch, usually `main`, as a normal Conventional Commit -- Do NOT commit on your own: make a commit only when the user explicitly asks for it, because the user reviews changes before every commit. When asked, commit only to the current branch (Conventional Commits) -- Do NOT merge, rebase onto `main`, push, or delete branches: merging and cleaning up branches is done by the user -- There are no pull requests: the user works with a local repository only +- Unit and integration tests live next to the code (`*.spec.ts`, `*.integration.spec.ts`); API and e2e tests live in `tests/`. +- Tests that need PocketBase use a temporary instance, not `pocketbase/pb_data`. +- Always verify data isolation with two users. +- Other rules — `docs/testing.md`. + +## Documentation + +- When you change or add a part of the app, in the same change update or create its page in `docs/guide/` using the template from `docs/README.md` and add it to the table of contents. +- If a decision changes the architecture, the data model, or the way of testing, update `docs/architecture.md`, `docs/data-model.md`, or `docs/testing.md`. +- A new command, environment variable, or startup step — update `README.md`. + +## Git + +- GitHub Flow: there is NO separate `develop` branch. `main` is the only long-lived branch +- Every working branch is created directly from a fresh `main`: features, fixes, and release preparation +- Branches are named by purpose in kebab-case: `feat/`, `fix/`, `release/` +- Do NOT create branches yourself: the user creates a branch (by the rules above) when the task is large (a feature, many related changes). Otherwise work in the current branch +- Small targeted changes (settings, adding a dependency, minor fixes) do not need a separate branch: they go directly to the current branch, usually `main`, as a regular Conventional Commit +- Do NOT commit yourself: commit only at the user's explicit request, because the user reviews changes before every commit. When asked, commit only to the current branch (Conventional Commits) +- Do NOT merge, rebase onto `main`, push, or delete branches: merging and branch cleanup are done by the user +- There are no pull requests: the user works only with the local repository diff --git a/README.md b/README.md index cf3f1ee..374f19a 100644 --- a/README.md +++ b/README.md @@ -1,102 +1,111 @@ # Pchelpcrm -A CRM for handling service requests (orders) of a private service professional: computer help, home repairs, furniture assembly, plumbing, electrical work and similar jobs. The domain is generic, so any business where a client asks for a job to be done fits. +CRM для частных мастеров: учёт клиентов и заявок для компьютерной помощи, мелкого ремонта, сборки мебели, сантехники, электрики и любых других работ «клиент обратился — работа выполнена». -## Concepts +Система многопользовательская: каждый мастер регистрируется сам и работает только со своими данными. -- **User** — the account owner who works in the CRM. Signs in with email and password (PocketBase auth). Data of each user is isolated from others. -- **Client** — a customer of the user. -- **Request** — a service request (order). Belongs to one client; a client can have many requests. -- **Category** — a request category. Dynamic: created and edited by the user. +## Возможности -## Stack +- Регистрация и вход по email и паролю. +- Клиенты: телефоны, контакты, адрес, ссылки на профили на площадках, заметка; поиск по имени и телефону; история заявок клиента. +- Заявки: статус, дата визита, адрес, сумма, что сделано, примечания мастера, отзыв клиента, ссылка на заказ на площадке, фото и аудио. +- Метки вместо категорий и источников: мастер сам заводит «Установка Windows», «YouDo», «Сарафан» и вешает на заявку сколько угодно. +- Поиск и фильтры заявок по тексту, статусу, меткам и периоду. +- Публичная ссылка на заявку для клиента: суть работы без данных клиента и вложений. +- Серверный рендер главной, карточки заявки и публичной страницы. -- [Angular](https://angular.dev/) — frontend, generated with [Angular CLI](https://github.com/angular/angular-cli) 22.2.3 -- [PocketBase](https://pocketbase.io/) — backend, database and authentication -- [pnpm](https://pnpm.io/) — package manager +## Понятия -## Distribution +| Понятие | Что это | +| --- | --- | +| Пользователь | мастер, владелец аккаунта | +| Клиент | заказчик мастера | +| Заявка (`Order`) | заказ; принадлежит одному клиенту, у клиента может быть много заявок | +| Метка (`Tag`) | ярлык заявки, который мастер создаёт сам | +| Публичная ссылка | страница заявки для клиента по случайному токену | -After the first release the application will be shipped as a single Docker container (the app and PocketBase together, no docker-compose) that can be deployed to a production server. +## Стек -## Development server +- [Angular](https://angular.dev/) 22 с SSR, чистый CSS +- [PocketBase](https://pocketbase.io/) 0.40 — база данных, аутентификация, файлы и API +- [pnpm](https://pnpm.io/) +- [Vitest](https://vitest.dev/) и [Playwright](https://playwright.dev/) — тесты -To start a local development server, run: +## Быстрый старт + +Нужны Node.js 22+, pnpm и бинарник [`pocketbase`](https://pocketbase.io/docs/) 0.40.x в `PATH`. ```bash -ng serve +pnpm install +cp .env.example .env # заполнить PB_SUPERUSER_EMAIL и PB_SUPERUSER_PASSWORD +pnpm pb:superuser # создать суперпользователя PocketBase +pnpm pb:serve # PocketBase на http://127.0.0.1:8090, админка — /_/ +pnpm start # приложение на http://localhost:4200/ ``` -Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. +В разработке браузер обращается к PocketBase по пути `/pb/*`, его проксирует dev-сервер Angular. -## Code scaffolding +## Команды -Angular CLI includes powerful code scaffolding tools. To generate a new component, run: +| Команда | Что делает | +| --- | --- | +| `pnpm start` | dev-сервер приложения | +| `pnpm build` | production-сборка в `dist/` | +| `pnpm lint` | ESLint | +| `pnpm test` | unit-тесты | +| `pnpm test:api` | API-тесты PocketBase: схема, правила доступа, изоляция пользователей | +| `pnpm test:integration` | репозитории приложения против тестового PocketBase | +| `pnpm e2e` | сценарии в браузере и проверка доступности (axe) | +| `pnpm e2e:prod` | сценарии на production-сборке | +| `pnpm pb:serve` | PocketBase для разработки | +| `pnpm pb:migrate` | применить миграции | +| `pnpm pb:superuser` | создать суперпользователя или сбросить его пароль | -```bash -ng generate component component-name +Тесты, которым нужен PocketBase, сами поднимают временный экземпляр и не трогают `pocketbase/pb_data`. Перед первым запуском e2e установите браузер: `pnpm exec playwright install chromium`. + +## Структура репозитория + +``` +src/ + app/ приложение Angular, по папке на фичу (auth, clients, orders, tags, share, …) + server.ts Node SSR-сервер +pocketbase/ + pb_migrations/ схема базы данных (JS-миграции) + pb_hooks/ кастомные маршруты и хуки PocketBase + pb_data/ локальная база (не в git) +tests/ + api/ API-тесты PocketBase + e2e/ браузерные тесты + support/ тестовый PocketBase и хелперы +scripts/ служебные скрипты +docs/ документация разработчика ``` -For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: +## Документация + +Документация разработчика лежит в [docs/](docs/README.md): + +- [architecture.md](docs/architecture.md) — общая схема, слои, DI, SSR и изоляция данных, nginx, принципы UI; +- [data-model.md](docs/data-model.md) — коллекции PocketBase, поля и правила доступа; +- [testing.md](docs/testing.md) — уровни тестов и тестовый PocketBase; +- `docs/guide/` — руководство по каждой части приложения: где лежит код, как им пользоваться и как его расширить. Пополняется вместе с кодом. + +## Production + +Схема: nginx отдаёт `/pb/*` в PocketBase (префикс `/pb` срезается), всё остальное — в Node SSR-сервер. Пример конфигурации nginx — в [docs/architecture.md](docs/architecture.md#7-url-pocketbase). ```bash -ng generate --help -``` - -## Building - -To build the project run: - -```bash -ng build -``` - -This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. - -## Running unit tests - -To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command: - -```bash -ng test -``` - -## Running end-to-end tests - -For end-to-end (e2e) testing, run: - -```bash -ng e2e -``` - -Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. - -## PocketBase - -The backend is [PocketBase](https://pocketbase.io/) (v0.40.x). The `pocketbase` binary must be in `PATH`. - -- `pocketbase/pb_migrations/` — collection schema as JS migrations (committed). -- `pocketbase/pb_data/` — local dev database (not committed). - -Local development: - -```bash -cp .env.example .env # then set PB_SUPERUSER_EMAIL / PB_SUPERUSER_PASSWORD -pnpm pb:superuser # create the superuser or reset its password (safe to re-run) -pnpm pb:serve # http://127.0.0.1:8090, admin UI at /_/ -``` - -Migrations are applied on `pb:serve` start (or explicitly with `pnpm pb:migrate`). Schema changes made in the dev admin UI are written to `pocketbase/pb_migrations/` automatically — commit them. - -Production uses the same binary version and the same `pb_migrations/`, with its own data dir: - -```bash -pocketbase serve --dir /path/to/pb_data --migrationsDir pocketbase/pb_migrations --automigrate=false --http 127.0.0.1:8090 +pnpm build +pocketbase serve --dir /path/to/pb_data --migrationsDir pocketbase/pb_migrations --hooksDir pocketbase/pb_hooks --automigrate=false --http 127.0.0.1:8090 PB_DIR=/path/to/pb_data node --env-file=.env scripts/pb-superuser.mjs +PB_URL=http://127.0.0.1:8090 PORT=4000 node dist/pchelpcrm/server/server.mjs ``` -Back up by copying the `pb_data` directory. +| Переменная | Назначение | +| --- | --- | +| `PB_URL` | внутренний адрес PocketBase для SSR-сервера и dev-прокси, по умолчанию `http://127.0.0.1:8090` | +| `PORT` | порт Node SSR-сервера, по умолчанию `4000` | +| `PB_DIR` | папка данных PocketBase для скрипта `pb:superuser` | +| `PB_SUPERUSER_EMAIL`, `PB_SUPERUSER_PASSWORD` | суперпользователь PocketBase (скрипт `pb:superuser` и тесты) | -## Additional Resources - -For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. +Резервная копия — копирование папки `pb_data`. После первого релиза приложение планируется поставлять одним Docker-контейнером (SSR-сервер и PocketBase вместе). diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..acd9557 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,62 @@ +# Документация разработчика + +Здесь собрано всё, что нужно, чтобы разобраться в коде Pchelpcrm и начать его менять: решения, модель данных и руководство по каждой части приложения. Руководство растёт вместе с кодом: каждая новая фича добавляет или обновляет свою страницу. + +## Основы + +| Документ | О чём | +| --- | --- | +| [architecture.md](architecture.md) | общая схема, слои внутри фичи, DI, SSR и изоляция данных, nginx, UI-принципы | +| [data-model.md](data-model.md) | коллекции PocketBase, поля, правила доступа, публичный маршрут | +| [testing.md](testing.md) | уровни тестов, тестовый PocketBase, контрактные тесты | + +## Руководство (`guide/`) + +Страницы появляются вместе с соответствующим кодом. + +| Страница | О чём | +| --- | --- | +| `guide/project-structure.md` | карта репозитория: `src/app`, `pocketbase/`, `tests/`, `scripts/`; где что искать | +| `guide/adding-a-feature.md` | пошаговый пример добавления новой сущности от миграции до экрана (на примере меток) | +| `guide/shared.md` | общая инфраструктура: клиент PocketBase, доменные ошибки, общие UI-компоненты | +| `guide/auth.md` | вход, регистрация, сессия, cookie, guard'ы, работа с текущим пользователем | +| `guide/tags.md` | метки: репозиторий, диалог управления, компонент выбора | +| `guide/clients.md` | клиенты: телефоны, поиск, форма, точки расширения карточки | +| `guide/orders.md` | заявки: статусы, фильтры, форма, вложения | +| `guide/share.md` | публичная ссылка и публичная страница | +| `guide/ssr.md` | как устроен серверный рендер: cookie, TransferState, что можно и нельзя на сервере | +| `guide/ui.md` | дизайн-токены, темы, общие компоненты, правила доступности | + +## Как писать страницу руководства + +Страница описывает часть приложения так, чтобы новый разработчик мог ей пользоваться, не читая весь код. + +```markdown +# <Фича> + +Одно-два предложения: что делает фича и для чего. + +## Где лежит +Дерево папок фичи с пояснением каждой важной папки и файла. + +## Публичный API +Что экспортирует `index.ts`: абстрактные классы, типы, компоненты (селектор, входы, выходы), функции `provideXxx()`, токены расширения. По каждому — одна строка назначения. + +## Как пользоваться +Короткие примеры кода для типовых задач: получить данные, встроить компонент, обработать ошибку. + +## Как расширить или заменить +Как подменить реализацию через DI, как подключиться к точке расширения, что поменять при добавлении поля. + +## Тесты +Где лежат тесты фичи, какой контракт у репозитория, какие фейки доступны. + +## Подводные камни +Неочевидное: ограничения SSR, правила доступа, особенности PocketBase. +``` + +Правила: + +- Пиши о том, что есть в коде сейчас, а не о планах. Если код меняется — страница меняется в том же коммите. +- Примеры кода должны быть рабочими и короткими; полный код — ссылкой на файл. +- Не дублируй `architecture.md` и `data-model.md`: ссылайся на нужный раздел. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..cdaa7c3 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,219 @@ +# Архитектура + +Архитектурные решения проекта. Если код или новая задача им противоречит, сначала обсуждается и правится этот документ. + +## 1. Общая схема + +``` + ┌─ location /pb/ ─(префикс /pb срезается)─► PocketBase :8090 +Браузер ──► nginx ──┤ ▲ + └─ location / ──► Node SSR-сервер :4200 ────────┘ + (src/server.ts) PB_URL — напрямую, без nginx +``` + +- Браузер ходит в PocketBase по пути `/pb/*` своего origin: SDK создаётся с адресом `/pb`, запрос `/pb/api/collections/...` nginx отправляет в PocketBase как `/api/collections/...`. +- Node SSR-сервер при рендере обращается к PocketBase напрямую по внутреннему адресу `PB_URL`. +- В разработке nginx нет: его роль для `/pb/*` выполняет прокси dev-сервера Angular (`proxy.conf.mjs`). + +- **PocketBase** — единственный бэкенд: база, авторизация, файлы, правила доступа (API rules), кастомные маршруты (`pocketbase/pb_hooks/`). Это же и публичный API для будущих клиентов (мобильное приложение, Telegram-бот, десктоп). +- **Безопасность данных обеспечивает PocketBase**, а не фронтенд. Фронтенд может быть скомпрометирован или заменён другим клиентом, поэтому изоляция пользователей проверяется API-тестами на уровне правил коллекций. +- **SSR-сервер** только рендерит страницы и отдаёт статику, API PocketBase он не проксирует. Браузер всегда обращается к своему origin (nginx): нет CORS, cookie общая для приложения. + +## 2. Слои и структура папок + +Код организован по фичам (`src/app//`). Внутри фичи — слои в духе DDD: + +``` +src/app/orders/ + index.ts ← публичный API фичи: что можно импортировать другим фичам + orders.routes.ts ← маршруты фичи (lazy) + provide-orders.ts ← provideOrders(): регистрация реализаций в DI + domain/ ← чистый TypeScript, без Angular и без PocketBase + order.ts ← модель (типы) + order-status.ts ← статусы и подписи + order-filter.ts ← параметры фильтрации/поиска + order-repository.ts ← abstract class OrderRepository (контракт и DI-токен) + data/ ← реализация контрактов через PocketBase + pocketbase-order-repository.ts + order-mapper.ts ← запись PocketBase ⇄ доменная модель + ui/ ← компоненты (одна папка на компонент, если есть html/css) + order-list/order-list.ts + order-form/order-form.ts + testing/ ← фейки для тестов других слоёв и фич + in-memory-order-repository.ts +``` + +Правила: + +1. **domain** не импортирует ничего из `@angular/*`, `pocketbase`, `data/`, `ui/`. Здесь только типы, чистые функции и абстрактные классы. +2. **data** знает про PocketBase и домен. Наружу отдаёт только доменные типы: запись PocketBase (`RecordModel`) не покидает слой `data`. +3. **ui** знает только домен и абстрактные классы. Компоненты никогда не импортируют `pocketbase` и `data/`. +4. Фича импортирует другую фичу **только через её `index.ts`**. Глубокие импорты (`../clients/data/...`) запрещены. +5. Общая инфраструктура, нужная всем фичам, живёт в `src/app/shared/` (например `shared/pocketbase/`, позже `shared/ui/`). Туда кладём только то, что реально используется двумя и более фичами. + +### Фичи MVP + +| Папка | Назначение | +| -------------------- | ---------------------------------------------------------------------------------------- | +| `shared/pocketbase/` | URL PocketBase, фабрика клиента, синхронизация cookie, маппинг ошибок | +| `users/` | сущность User (профиль владельца аккаунта), без логики входа | +| `auth/` | сессия: вход, регистрация, выход, текущий пользователь, guard'ы, формы входа/регистрации | +| `layout/` | каркас приложения: шапка, навигация, меню пользователя | +| `home/` | главная страница: лендинг для гостя или список заявок для вошедшего | +| `tags/` | метки пользователя: диалог управления метками и компонент выбора меток | +| `clients/` | клиенты | +| `orders/` | заявки, вложения | +| `share/` | публичная ссылка и публичная страница заявки | +| `not-found/` | страница 404 | + +### Направление зависимостей между фичами + +``` +home ──► orders ──► clients ──► shared + │ │ └───► tags ─────► shared + │ └──────► share ────► shared + └──► auth ──► users +``` + +- Зависимости идут только в одну сторону, циклов нет. +- Если «нижней» фиче нужно показать что-то из «верхней» (например, история заявок в карточке клиента), нижняя объявляет точку расширения — `InjectionToken` со списком компонентов, — а верхняя регистрирует в ней свой компонент в `provideXxx()`. Так `clients` ничего не знает о заявках. + +> Сущность заявки в коде называется **`Order`**; коллекция, папка и маршруты — `orders`. + +## 3. Dependency Injection через абстрактные классы + +Каждый репозиторий и сервис, у которого может появиться другая реализация, описан **абстрактным классом**. Абстрактный класс одновременно контракт и DI-токен. + +```ts +// tags/domain/tag-repository.ts +export abstract class TagRepository { + abstract list(): Promise; + abstract create(draft: TagDraft): Promise; + abstract update(id: string, changes: Partial): Promise; + abstract delete(id: string): Promise; +} + +// tags/provide-tags.ts +export function provideTags(): Provider[] { + return [{ provide: TagRepository, useClass: PocketBaseTagRepository }]; +} + +// app.config.ts — единственное место, где выбираются реализации +providers: [ + providePocketBase(), + provideAuth(), + provideTags(), + provideClients(), + provideOrders(), + provideShare(), +]; +``` + +- Компоненты и сервисы получают зависимость так: `private readonly tags = inject(TagRepository);`. +- Чтобы поменять реализацию, достаточно изменить `provideXxx()` или `app.config.ts`. +- В unit-тестах вместо PocketBase подставляется фейк из `testing/`: `{ provide: TagRepository, useClass: InMemoryTagRepository }`. +- Методы репозиториев возвращают `Promise` (PocketBase SDK промис-ориентирован). В компонентах данные загружаются через `resource()`: так SSR дожидается загрузки (resource регистрирует pending task), а состояние доступно как сигналы. +- Отдельного слоя сторов нет. Стор (сервис с состоянием на сигналах) заводится только там, где одно состояние реально разделяют несколько компонентов, например список меток для диалога управления, компонента выбора и фильтров. + +## 4. Ошибки + +`shared/pocketbase/` переводит `ClientResponseError` в доменные ошибки, которые понимает UI: + +| Доменная ошибка | Когда | +| ------------------- | -------------------------------------------------------------------------------- | +| `NotFoundError` | 404 (в том числе «чужая запись»: PocketBase не различает «нет» и «нет доступа») | +| `ValidationError` | 400 с ошибками по полям: `fieldErrors: Record` (коды PocketBase) | +| `UnauthorizedError` | 401/403: сессия недействительна | +| `NetworkError` | нет связи / PocketBase недоступен | + +UI показывает ошибки полей рядом с полями, а общие ошибки — в области уведомлений с `role="alert"`. + +## 5. Аутентификация + +- Вход и регистрация по email и паролю через коллекцию `users` (PocketBase auth). Регистрация открытая. Подтверждение email и сброс пароля в MVP не делаются. +- Токен PocketBase (JWT) хранится в **cookie `pb_auth`** (`Path=/`, `SameSite=Lax`, `Secure` на HTTPS). Cookie нужна, чтобы SSR-сервер знал, кто пришёл. +- Cookie **не `HttpOnly`**: браузерному SDK нужен токен для прямых запросов в `/pb/api`. Риск — кража токена при XSS. Снижаем его так: Angular экранирует шаблоны, нигде не используется `innerHTML` с пользовательскими данными, позже добавим CSP. Это осознанный компромисс MVP. +- В браузере: при изменении `authStore` cookie перезаписывается (`exportToCookie`); при старте приложения, если токен есть, вызывается `authRefresh()`; при ошибке — выход. +- На сервере: токен читается из заголовка `Cookie` текущего запроса (`loadFromCookie`). Сервер cookie не пишет и токен не обновляет. + +## 6. SSR и изоляция данных пользователей + +### Какие маршруты как рендерятся + +| Маршрут | Режим | Почему | +| --------------------------------------- | ------ | ------------------------------------------------- | +| `/` | Server | лендинг для гостя или список заявок для вошедшего | +| `/orders/:id` | Server | карточка заявки | +| `/p/:token` | Server | публичная страница заявки (превью в мессенджерах) | +| остальные (`/clients/**`, формы заявок) | Client | интерактивные формы, SSR не даёт выгоды | + +Настраивается в `src/app/app.routes.server.ts`. + +### Правила, без которых данные пользователей могут перемешаться + +На сервере каждый HTTP-запрос получает **свой экземпляр Angular-приложения и свой root-инжектор**. Всё, что живёт в DI, изолировано. Опасно только то, что живёт **вне DI** — на уровне модуля, общее для всех запросов. + +1. **Нельзя** создавать PocketBase на уровне модуля (`export const pb = new PocketBase(...)`). Экземпляр создаёт только фабрика в `shared/pocketbase/` внутри DI. ESLint-правило запрещает `new PocketBase(` во всех остальных файлах. +2. **Нельзя** хранить пользовательские данные в переменных модуля, статических полях классов, кэшах вне DI. +3. Сервер рендерит страницы с заголовками `Cache-Control: private, no-store` и `Vary: Cookie`: ни браузер, ни прокси не должны отдать одну страницу двум людям. +4. На сервере используется глобальный `fetch` без общего состояния; `authStore` — свой у каждого экземпляра PocketBase (не `LocalAuthStore`, а in-memory `BaseAuthStore`). +5. Защищённые файлы (вложения) не рендерятся на сервере: их URL содержит короткоживущий токен, ему не место в HTML. +6. Изоляция проверяется e2e-тестами: параллельные SSR-запросы двух пользователей не видят данных друг друга. + +### Гидратация + +PocketBase SDK использует `fetch`, а не `HttpClient`, поэтому встроенный HTTP transfer cache Angular не работает. Чтобы после гидратации не загружать данные второй раз, используется `TransferState`. + +## 7. URL PocketBase + +- **Сервер:** `PB_URL` — переменная окружения Node-сервера, внутренний адрес PocketBase для серверного рендера (по умолчанию `http://127.0.0.1:8090`). В браузер не передаётся. +- **Браузер:** `/pb`. Префикс `/pb` — константа `PB_PUBLIC_PATH` в `shared/pocketbase/`; если путь в nginx поменяется, меняется только она. +- Всё это задаётся в одном месте: `shared/pocketbase/` (InjectionToken `PB_URL`, разные провайдеры для сервера и браузера). +- **Разработка:** `ng serve` проксирует `/pb/*` → `PB_URL` со срезанием префикса (`proxy.conf.mjs`). E2E-тесты production-сборки используют маленький тестовый фронт-прокси, повторяющий правила nginx. + +### Ожидаемая конфигурация nginx (пример, деплой — вне MVP) + +```nginx +location /pb/ { + proxy_pass http://127.0.0.1:8090/; # слэш в конце срезает префикс /pb + proxy_http_version 1.1; + proxy_set_header Connection ''; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; # для realtime (SSE) PocketBase + proxy_read_timeout 360s; + client_max_body_size 60M; # один файл вложения до 50 МБ + запас +} + +location / { + proxy_pass http://127.0.0.1:4000; # Node SSR-сервер + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +} +``` + +- Вложения загружаются по одному файлу на запрос, чтобы укладываться в `client_max_body_size`. +- В настройках PocketBase нужно указать доверенный заголовок реального IP (`X-Real-IP`), иначе лимиты запросов и логи будут видеть IP nginx. +- Админку PocketBase удобнее открывать напрямую на порту 8090 (через SSH-туннель), а не через `/pb/_/`. + +## 8. Формы и UI + +- Формы — Signal Forms (`@angular/forms/signals`) со схемами валидации. +- Стили — чистый CSS: дизайн-токены в CSS custom properties (`src/styles.css`), тёмная тема по `prefers-color-scheme`, никаких препроцессоров и CSS-фреймворков. +- Префикс селекторов компонентов и директив — `ksk` (`ksk-order-list`, `kskAutofocus`). +- Минимальная нагрузка на GPU — приложение должно быть лёгким на слабых телефонах и ноутбуках: + - анимируются только `transform` и `opacity`, коротко (до 200 мс); при `prefers-reduced-motion: reduce` анимации отключаются; + - без `backdrop-filter`, `filter: blur()`, больших и многослойных `box-shadow`, анимированных градиентов, `background-attachment: fixed`; + - `will-change` — только точечно и временно, не «на всякий случай»; + - тени — одна мягкая тень из токенов; глубину лучше передавать фоном поверхности и границей; + - длинные списки — `content-visibility: auto` с `contain-intrinsic-size`, чтобы не рендерить невидимое. +- Интерфейс только на русском, без i18n-фреймворка. Даты и суммы форматируются в локали `ru-RU`, валюта — рубли. +- Mobile-first: мастер часто работает с телефона. +- Доступность: AXE без ошибок, WCAG AA. + +## 9. Что сознательно отложено + +Подтверждение email и сброс пароля, инвайты, публичный профиль мастера, отчёты и финансы, напоминания, импорт/экспорт, Docker и деплой, мобильное приложение, бот. Архитектура их не блокирует: всё общение с данными идёт через PocketBase API и его правила. diff --git a/docs/data-model.md b/docs/data-model.md new file mode 100644 index 0000000..a44ad47 --- /dev/null +++ b/docs/data-model.md @@ -0,0 +1,148 @@ +# Модель данных (PocketBase) + +Схема создаётся JS-миграциями в `pocketbase/pb_migrations/` (одна миграция на изменение, пишется руками, не через админку). Кастомные маршруты и хуки — в `pocketbase/pb_hooks/`. + +У всех коллекций, кроме `users`, есть поле `owner` — ссылка на владельца. Это основа изоляции: любые правила доступа начинаются с `owner = @request.auth.id`. + +Поля `id`, `created`, `updated` (autodate) есть у всех коллекций и ниже не перечисляются. + +## users (auth-коллекция, уже существует в PocketBase) + +| Поле | Тип | Ограничения | +| --- | --- | --- | +| `email` | email | обязательно, уникально | +| `password` | password | минимум 8 символов | +| `name` | text | необязательно, до 100 символов | +| `verified` | bool | системное; гость не может выставить `true` | + +Способ входа — только email + пароль (OAuth2, OTP, MFA выключены). + +| Правило | Значение | Смысл | +| --- | --- | --- | +| list / view | `id = @request.auth.id` | пользователь видит только себя | +| create | `""` (всем) | открытая регистрация | +| update | `id = @request.auth.id` | меняет только себя | +| delete | `null` (только суперпользователь) | удаление аккаунта в MVP не делаем | + +Поле `avatar`, созданное PocketBase по умолчанию, можно оставить, в MVP оно не используется. + +## tags — метки + +Метки заменяют и категории, и источники заявок: «Установка Windows», «YouDo», «Профи.ру», «Сарафан», «Гарантия» — это всё метки. Набор у каждого пользователя свой. + +| Поле | Тип | Ограничения | +| --- | --- | --- | +| `owner` | relation → users | обязательно, одна, `cascadeDelete: true` | +| `name` | text | обязательно, 1–50 символов | +| `color` | select | одно из: `gray`, `red`, `orange`, `yellow`, `green`, `teal`, `blue`, `purple`, `pink`; обязательно | + +Индекс: уникальный `(owner, name COLLATE NOCASE)` — у одного пользователя нет двух меток с одинаковым названием без учёта регистра; у разных пользователей названия могут совпадать. + +Цвет — ключ из фиксированной палитры, а не произвольный HEX: так UI гарантирует контраст AA в светлой и тёмной теме. + +## clients — клиенты + +| Поле | Тип | Ограничения | +| --- | --- | --- | +| `owner` | relation → users | обязательно, `cascadeDelete: true` | +| `name` | text | обязательно, 1–200 символов (как клиента зовут или как он подписан на площадке) | +| `phones` | json | массив нормализованных номеров (`["79991234567"]`), до 5 штук, можно пустой | +| `email` | email | необязательно | +| `contacts` | text | свободный текст до 2000 символов: мессенджеры и прочие контакты | +| `external_urls` | json | необязательно; массив ссылок на профили клиента на любых площадках (YouDo, Профи.ру, Avito и др.), до 5 штук, каждая — валидный `http(s)` URL | +| `address` | text | необязательно, до 500 символов | +| `note` | text | внутренняя заметка, до 5000 символов | + +Нормализация телефона (делает фронтенд, функция в `clients/domain`): оставить только цифры; `8XXXXXXXXXX` → `7XXXXXXXXXX`; 10 цифр, начинающиеся на `9`, → добавить `7`. Остальные номера хранятся как есть (только цифры). + +Удалить клиента, у которого есть заявки, нельзя (связь `orders.client` обязательная и без каскада): сначала удаляются или переносятся заявки. + +## orders — заявки + +| Поле | Тип | Ограничения | +| --- | --- | --- | +| `owner` | relation → users | обязательно, `cascadeDelete: true` | +| `client` | relation → clients | обязательно, одна, без каскада | +| `title` | text | обязательно, 1–200 символов | +| `description` | text | описание проблемы со слов клиента, до 10000 | +| `status` | select | обязательно: `new`, `scheduled`, `in_progress`, `done`, `failed` | +| `scheduled_at` | date | дата и время визита, необязательно | +| `address` | text | адрес выезда, до 500 (по умолчанию в форме подставляется адрес клиента) | +| `price` | number | целые рубли, ≥ 0, необязательно | +| `result` | text | что сделано, до 10000; **виден по публичной ссылке** | +| `note` | text | примечания мастера, до 10000; **видны по публичной ссылке** | +| `review` | text | отзыв клиента (например, с площадки), до 5000; **виден по публичной ссылке** | +| `external_url` | url | необязательно; ссылка на страницу заказа на любой площадке (YouDo, Профи.ру, Avito и др.) | +| `tags` | relation → tags | несколько, до 20, необязательно | +| `attachments` | file | несколько, до 20 файлов, до 50 МБ каждый, **protected**; изображения (`image/jpeg`, `image/png`, `image/webp`, `image/heic`) и аудио (`audio/mpeg`, `audio/mp4`, `audio/x-m4a`, `audio/aac`, `audio/ogg`, `audio/wav`, `audio/webm`, `audio/amr`); миниатюры `200x200` | +| `share_token` | text | необязательно; если задан — 43 символа `[A-Za-z0-9_-]`; уникален среди непустых | + +Статусы и подписи в UI: + +| Значение | Подпись | +| --- | --- | +| `new` | Новая | +| `scheduled` | Запланирована | +| `in_progress` | В работе | +| `done` | Выполнена | +| `failed` | Не выполнена | + +Переходы между статусами не ограничены: мастер может выставить любой. + +Индексы: `owner`, `(owner, status)`, `(owner, created)`, уникальный частичный `share_token WHERE share_token != ''`. + +### Соответствие полям YouDo (для справки) + +| YouDo | Pchelpcrm | +| --- | --- | +| title, description | `title`, `description` | +| price | `price` | +| address, privateInfo | `address` | +| customerName | `clients.name` | +| customerUrl | `clients.external_urls` | +| taskUrl | `external_url` | +| category | метка | +| review | `review` | +| taskDate | `created` / `scheduled_at` | + +## Правила доступа для tags, clients, orders + +Общий шаблон (для `orders` добавляются проверки связей): + +| Правило | Значение | +| --- | --- | +| list / view | `owner = @request.auth.id` | +| create | `@request.auth.id != "" && owner = @request.auth.id` | +| update | `owner = @request.auth.id && (@request.body.owner:isset = false \|\| @request.body.owner = @request.auth.id)` | +| delete | `owner = @request.auth.id` | + +Дополнительно для `orders` (create и update): + +- клиент принадлежит тому же пользователю: `client.owner = @request.auth.id`; +- все метки принадлежат тому же пользователю (для пустого списка условие выполняется). + +Если какое-то условие невозможно выразить правилом, его реализуют хуком в `pocketbase/pb_hooks/`. Решение принимается при реализации, поведение фиксируют тесты. + +### Как PocketBase отвечает на нарушения правил + +API-тесты проверяют именно это поведение. Если фактический код в PocketBase 0.40 окажется другим, тесты фиксируют фактический, а главное требование неизменно: **чужие данные никогда не попадают в ответ и не меняются**. + +| Операция | Правило не выполнено | Правило `null` (не суперпользователь) | +| --- | --- | --- | +| list | 200, чужие записи отфильтрованы | 403 | +| view | 404 | 403 | +| create | 400 | 403 | +| update | 404 | 403 | +| delete | 404 | 403 | + +## Публичный маршрут + +`GET /api/public/orders/{token}` (хук в `pocketbase/pb_hooks/`), без авторизации. + +- Ищет заявку по непустому `share_token`, совпадающему точно. +- Ответ 200 содержит **только**: `title`, `description`, `result`, `note`, `review`, `status`, `scheduled_at`, `price`, `created`, `tags: [{ name, color }]`. +- Никогда не содержит: `id` заявки, `owner`, данные клиента (имя, телефоны, контакты, адрес, ссылки на профили), `address` заявки, `external_url`, `attachments`, `share_token`, email мастера. +- Нет заявки или токен пустой/неверного формата — 404 с одинаковым телом. +- Заголовки: `Cache-Control: no-store`, `X-Robots-Tag: noindex`. + +Ссылку создаёт владелец, записывая в `share_token` случайную строку (32 случайных байта в base64url, сгенерированные в браузере через `crypto.getRandomValues`). Отключить ссылку — записать пустую строку; перевыпустить — записать новую. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..6585211 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,56 @@ +# Тестирование + +Разработка ведётся по TDD: тесты пишутся до реализации. + +## Уровни тестов + +| Уровень | Что проверяет | Где лежит | Команда | +| --- | --- | --- | --- | +| **API** (e2e бэкенда) | схема, миграции, правила доступа, изоляция пользователей, хуки PocketBase | `tests/api/**/*.api.spec.ts` | `pnpm test:api` | +| **Unit** | доменные функции, компоненты и сервисы с фейками вместо PocketBase | рядом с кодом, `src/**/*.spec.ts` | `pnpm test` | +| **Integration** | PocketBase-реализации репозиториев и сервисов против настоящего PocketBase | рядом с кодом, `src/**/*.integration.spec.ts` | `pnpm test:integration` | +| **E2E** | пользовательские сценарии в браузере, SSR, доступность (axe) | `tests/e2e/**/*.e2e.ts` | `pnpm e2e` | +| **E2E prod** | сценарии на production-сборке, включая изоляцию данных при SSR | `tests/e2e/` (проект `ssr-prod`) | `pnpm e2e:prod` | + +Команды появляются по мере того, как настраиваются соответствующие стенды. + +## Контрактные тесты репозиториев + +У каждого репозитория две реализации: PocketBase (`data/`) и фейк в памяти (`testing/`). Чтобы фейк не расходился с реальностью, поведение описывается **один раз**: + +```ts +// tags/testing/tag-repository.contract.ts +export function describeTagRepositoryContract( + setup: () => Promise<{ repoA: TagRepository; repoB: TagRepository }>, +) { + describe('контракт TagRepository', () => { + it('не показывает пользователю B метки пользователя A', async () => { /* ... */ }); + }); +} +``` + +и вызывается дважды: в `in-memory-tag-repository.spec.ts` (быстро, в `pnpm test`) и в `pocketbase-tag-repository.integration.spec.ts` (на тестовом PocketBase, в `pnpm test:integration`). + +## Тестовый PocketBase + +API-, integration- и e2e-тесты работают с **отдельным временным экземпляром** PocketBase, а не с `pocketbase/pb_data`: + +1. Создаётся временная папка данных. +2. Создаётся суперпользователь (`pocketbase superuser upsert`) с данными из `.env` (`PB_SUPERUSER_EMAIL`, `PB_SUPERUSER_PASSWORD`). +3. Запускается `pocketbase serve` на свободном порту с `--dir <временная папка> --migrationsDir pocketbase/pb_migrations --hooksDir pocketbase/pb_hooks`; миграции применяются при старте. +4. Ожидание готовности по `GET /api/health`. +5. После прогона процесс останавливается, папка удаляется. + +Код запуска один на все уровни: `tests/support/pocketbase-server.ts`. + +## Правила написания тестов + +- **Каждый тест создаёт своих пользователей** с уникальными email (`user-<случайная строка>@test.local`). Тесты не зависят друг от друга и от порядка, поэтому могут идти параллельно и не нуждаются в очистке. +- Хелперы (`tests/support/`): `createUser()`, `asUser(user)`, `asGuest()`, `asSuperuser()` возвращают готовый экземпляр PocketBase SDK. +- **Изоляцию проверяем всегда двумя пользователями**: A создаёт данные, B пытается их прочитать, найти через list/filter, изменить, удалить, сослаться на них. Плюс гость. +- Тест проверяет поведение, а не реализацию: статус ответа, тело, состояние после операции (перечитать суперпользователем). +- Unit-тесты компонентов используют фейки из `/testing/` и не обращаются к сети. +- Компонентные тесты ищут элементы по ролям и подписям (как пользователь и скринридер), а не по CSS-классам. +- E2E-тесты: каждый сценарий регистрирует своего пользователя через UI или API; на ключевых страницах выполняется проверка axe (`@axe-core/playwright`) без нарушений. +- Названия тестов на русском и описывают поведение: `it('не отдаёт гостю чужую метку')`. +- Пока реализации нет, тесты должны компилироваться: для этого создаются минимальные заглушки контрактов (типы, абстрактные классы, методы с `throw new Error('Not implemented')`). Новые тесты падают из-за невыполненных ожиданий, а не из-за ошибок импорта или стенда.