docs: rewrite claude.md and readme in russian, add architecture docs

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
kamask
2026-10-25 12:35:01 +03:00
co-authored by Claude Sonnet 5.5
parent 0f69a2f8a5
commit 38b38de929
6 changed files with 686 additions and 154 deletions
+115 -77
View File
@@ -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). Read:
- **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.
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/<app part>.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/<feature>/` (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 (`<feature>.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 - 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 - Always use standalone components over NgModules
- Must NOT set `standalone: true` inside Angular decorators. It's the default in Angular v20+. - 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+. - Do NOT set `changeDetection: ChangeDetectionStrategy.OnPush` explicitly — `OnPush` is the default in Angular v22+.
- Use signals for state management - The selector prefix for components and directives is `ksk` (`ksk-order-list`, `kskAutofocus`)
- Implement lazy loading for feature routes - Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings in the `host` object of the `@Component` or `@Directive` decorator
- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead
- Use `NgOptimizedImage` for all static images. - Use `NgOptimizedImage` for all static images.
- `NgOptimizedImage` does not work for inline base64 images. - `NgOptimizedImage` does not work for inline base64 images.
## Accessibility Requirements ### Components
- 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
- Keep components small and focused on a single responsibility - Keep components small and focused on a single responsibility
- Use `input()` and `output()` functions instead of decorators - 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 `model()` for two-way binding with the `[(prop)]` syntax instead of pairing `input()` with `output()`
- Use `computed()` for derived state - Prefer inline templates for small components; move large templates and styles to separate files
- Use `linkedSignal()` for state derived from multiple reactive sources that must stay synchronized - In external templates/styles, use paths relative to the component's TS file.
- Prefer inline templates for small components - Do NOT import `CommonModule`; import only the directives and pipes the template uses, such as `AsyncPipe` or `DatePipe`
- 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.
## State Management ### State
- Use signals for local component state - Signals for local component state
- Use `computed()` for derived state - `computed()` for derived state
- `linkedSignal()` for state derived from multiple reactive sources that must stay synchronized
- Keep state transformations pure and predictable - 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 - Keep templates simple, without complex logic
- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch` - Native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch`
- Use the async pipe to handle observables - Do NOT use `ngClass`, use `class` bindings; do NOT use `ngStyle`, use `style` bindings
- Do not assume globals like (`new Date()`) are available. - 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 - 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
- Use the `providedIn: 'root'` option for singleton services - 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+) - Prefer the `@Service` decorator over `@Injectable({providedIn: 'root'})` for new singleton services (Angular v22+)
- Use the `inject()` function instead of constructor injection - Use the `inject()` function instead of constructor injection
## Routing ### Routing
- All routes are lazy loaded (`loadComponent` / `loadChildren`), including feature-internal ones - All routes are lazy-loaded (`loadComponent` / `loadChildren`), including the internal routes of features
- 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 - `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/<feature>/` (e.g. `src/app/clients/`), not global `components/`, `services/`, `pipes/` folders - The app MUST pass all AXE checks and meet WCAG AA minimums, including focus management, color contrast, and ARIA attributes.
- Keep everything a feature needs inside its folder: components, services, models, routes and tests - 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.
- One concept per file: one component, directive, pipe or service per file - 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`).
- 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`) - Skill recommendations must not violate the rules of this file; on conflict, this file wins.
- Use kebab-case file names. Do NOT add `.component` / `.service` suffixes (matches the existing `app.ts`) - 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`.
- Put unit tests next to the code they test (`*.spec.ts`) - 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").
- Each feature has its own routes file (`<feature>.routes.ts`) and is lazy loaded from `app.routes.ts` via `loadChildren` / `loadComponent` - The UI is in Russian only (no i18n framework); dates and amounts use the `ru-RU` locale, the currency is rubles. Mobile-first.
- 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
## Git Branches ## Tests
- Follow GitHub Flow: there is NO separate `develop` branch. `main` is the only long-lived branch - Unit and integration tests live next to the code (`*.spec.ts`, `*.integration.spec.ts`); API and e2e tests live in `tests/`.
- Create every working branch directly from the latest `main`: features, fixes and release preparation alike - Tests that need PocketBase use a temporary instance, not `pocketbase/pb_data`.
- Name branches by purpose in kebab-case: `feat/<short-name>`, `fix/<short-name>`, `release/<version>` - Always verify data isolation with two users.
- 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 - Other rules — `docs/testing.md`.
- 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) ## Documentation
- 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 - 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/<short-name>`, `fix/<short-name>`, `release/<version>`
- 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
+86 -77
View File
@@ -1,102 +1,111 @@
# Pchelpcrm # 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 ```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 Тесты, которым нужен PocketBase, сами поднимают временный экземпляр и не трогают `pocketbase/pb_data`. Перед первым запуском e2e установите браузер: `pnpm exec playwright install chromium`.
ng generate component component-name
## Структура репозитория
```
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 ```bash
ng generate --help 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
## 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
PB_DIR=/path/to/pb_data node --env-file=.env scripts/pb-superuser.mjs 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 Резервная копия — копирование папки `pb_data`. После первого релиза приложение планируется поставлять одним Docker-контейнером (SSR-сервер и PocketBase вместе).
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.
+62
View File
@@ -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`: ссылайся на нужный раздел.
+219
View File
@@ -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 создаётся с адресом `<origin>/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/<feature>/`). Внутри фичи — слои в духе 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<Tag[]>;
abstract create(draft: TagDraft): Promise<Tag>;
abstract update(id: string, changes: Partial<TagDraft>): Promise<Tag>;
abstract delete(id: string): Promise<void>;
}
// 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<string, string>` (коды 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`). В браузер не передаётся.
- **Браузер:** `<window.location.origin>/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 и его правила.
+148
View File
@@ -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`). Отключить ссылку — записать пустую строку; перевыпустить — записать новую.
+56
View File
@@ -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-тесты компонентов используют фейки из `<feature>/testing/` и не обращаются к сети.
- Компонентные тесты ищут элементы по ролям и подписям (как пользователь и скринридер), а не по CSS-классам.
- E2E-тесты: каждый сценарий регистрирует своего пользователя через UI или API; на ключевых страницах выполняется проверка axe (`@axe-core/playwright`) без нарушений.
- Названия тестов на русском и описывают поведение: `it('не отдаёт гостю чужую метку')`.
- Пока реализации нет, тесты должны компилироваться: для этого создаются минимальные заглушки контрактов (типы, абстрактные классы, методы с `throw new Error('Not implemented')`). Новые тесты падают из-за невыполненных ожиданий, а не из-за ошибок импорта или стенда.