mirror of
https://github.com/suitenumerique/docs.git
synced 2026-09-26 19:45:01 +02:00
Also the AGENTS.md is now track to have all the same one and track any modification made on it.
268 lines
16 KiB
Markdown
268 lines
16 KiB
Markdown
# Agent Guidelines for Docs
|
|
|
|
This file contains guidelines for AI agents coding in this repository.
|
|
|
|
## Project Overview
|
|
|
|
**La Suite Docs** is a collaborative text editor built by DINUM (French government) and ZenDiS (German government). It features real-time editing, offline support, AI actions, and multi-format export (PDF/DOCX/ODT).
|
|
|
|
Repository: https://github.com/suitenumerique/docs
|
|
|
|
## Monorepo Structure
|
|
|
|
- `src/backend/` - Django REST API (Python 3.14+)
|
|
- `src/frontend/apps/impress/` - Main Next.js 15 application (TypeScript)
|
|
- `src/frontend/apps/e2e/` - Playwright E2E tests
|
|
- `src/frontend/packages/i18n/` - Shared i18n utilities
|
|
- `src/frontend/packages/eslint-plugin-docs/` - Custom ESLint plugin
|
|
- `src/frontend/servers/y-provider/` - Conversion service only (Express, `POST /api/convert/`). It no longer serves any websocket
|
|
- `src/yhub-server/` - Collaboration server: a thin TypeScript wrapper around `@y/hub` (websockets, REST routes, worker). It holds the content of the documents
|
|
- `src/mail/` - Email templates (MJML)
|
|
- `src/helm/` - Kubernetes/Helm deployment
|
|
|
|
## Development Commands
|
|
|
|
### Setup and Services
|
|
|
|
```bash
|
|
make bootstrap # Full dev setup (build + migrate + demo + run)
|
|
make build # Build all Docker containers
|
|
make run # Start all services
|
|
make stop # Stop services
|
|
make build-yhub # Build the collaboration server (yhub) image
|
|
make migrate-yhub # Create/upgrade the yhub database schema (`yarn init-db`), safe to re-run
|
|
make status # Check running services
|
|
```
|
|
|
|
### Backend (Python/Django)
|
|
|
|
Tests run inside Docker containers. You must first build the backend image and ensure the `lasuite-network` Docker network exists before running tests.
|
|
|
|
```bash
|
|
make build-backend # Build the backend Docker image (required before first test run)
|
|
docker network create lasuite-network # Create the external network (required once)
|
|
bin/pytest -n auto # Run all backend tests in parallel
|
|
bin/pytest -n auto path/to/test # Run specific test file/directory
|
|
bin/pytest -n auto path/to/test.py::TestClass::test_method # Run single test via docker compose
|
|
make lint # ruff format + ruff check + pylint
|
|
make lint-ruff-format # Format only
|
|
make lint-ruff-check # Lint only
|
|
make migrate # Run database migrations
|
|
make makemigrations # Create new migrations
|
|
make resetdb # Flush DB + create superuser (admin/admin)
|
|
```
|
|
|
|
### Frontend (TypeScript/Next.js)
|
|
|
|
```bash
|
|
# From src/frontend/apps/impress/:
|
|
yarn dev # Development server (port 3000)
|
|
yarn build # Production build (includes prettier + stylelint checks)
|
|
yarn lint # TypeScript check + ESLint
|
|
yarn test # Run Vitest tests
|
|
yarn prettier # Format code
|
|
yarn stylelint # Lint CSS
|
|
|
|
# From project root:
|
|
make frontend-lint # Lint all frontend workspaces
|
|
make frontend-test # Run frontend tests
|
|
```
|
|
|
|
### Collaboration server (yhub, TypeScript)
|
|
|
|
```bash
|
|
# From src/yhub-server/ (standalone package: its own package.json and yarn.lock,
|
|
# not a workspace of src/frontend):
|
|
yarn test # Vitest unit tests (@y/hub is mocked, no store needed)
|
|
yarn typecheck # tsc --noEmit
|
|
yarn lint # ESLint
|
|
yarn build # Compile to dist/
|
|
yarn init-db # Create/upgrade the yhub postgres schema (what `make migrate-yhub` runs)
|
|
```
|
|
|
|
The container starts with `node --import ./dist/sentry.js dist/server.js`: keep the
|
|
`--import` when overriding the command. `src/yhub-server/README.md` is the reference
|
|
for everything this server does (permissions, roles, migration, storage, metrics).
|
|
|
|
### Mails
|
|
|
|
```bash
|
|
make mails-install # Install dependencies
|
|
make mails-build # Convert MJML to HTML + plaintext
|
|
```
|
|
|
|
### Helm/Kubernetes Deployment
|
|
|
|
```bash
|
|
make build-k8s-cluster # Create local Kind cluster
|
|
make start-tilt # Start Tilt for hot-reload
|
|
# From src/helm/: helmfile -n impress -e dev apply/destroy/diff
|
|
```
|
|
|
|
### Dev Service URLs
|
|
|
|
- Frontend: http://localhost:3000
|
|
- Backend API/Admin: http://localhost:8071
|
|
- Keycloak (auth): http://localhost:8083
|
|
- MinIO (S3): http://localhost:9000
|
|
- Collaboration server (yhub): http://localhost:3002 (websocket on `/collaboration/ws/v1/docs/{docid}`)
|
|
- yhub PostgreSQL: localhost:5433 (its own database, apart from the backend's on 15432)
|
|
- Mailcatcher: http://localhost:1081
|
|
|
|
## Architecture
|
|
|
|
### Backend
|
|
|
|
- **Framework**: Django + DRF, configured via `django-configurations` (`src/backend/impress/settings.py`)
|
|
- **Main app**: `src/backend/core/` (models, API viewsets, services, authentication)
|
|
- **API**: REST on `/api/v1.0/` with nested routes (e.g., `/documents/{id}/accesses/`)
|
|
- **Auth**: OIDC via `mozilla-django-oidc` (Keycloak in dev)
|
|
- **Background tasks**: Celery + Redis
|
|
- **Database**: PostgreSQL 16 with `django-treebeard` for document hierarchy
|
|
- **Storage**: S3-compatible (MinIO in dev) for attachments and media. **The content of a document is not stored by the backend anymore**: it lives in the collaboration server (yhub). `Document.content`, `save_content` and the S3 object versions are gone; `file_key` only survives as a pointer for the migration of legacy documents
|
|
- **Collaboration server client**: `core/services/yhub_services.py` (`YHubService`: `get_ydoc`, `create_ydoc`, `delete_ydoc`, `restore_ydoc`, `reset_ydoc`, `migrate`, `reset_connections`). It is called synchronously by document creation from a file, `duplicate`, `formatted-content`, and from Celery tasks for delete/restore/access changes. Mock it in tests (`CELERY_TASK_ALWAYS_EAGER=True` in the `Test` configuration, so tasks run inline)
|
|
- **Service-to-service auth**: short-lived RS256 JWTs with an `aud` claim, never a shared bearer token. The backend signs with `JWT_PRIVATE_KEY` (`core/services/jwt_services.py`) and publishes its keys on `/api/v1.0/jwks`; yhub signs with `YHUB_JWT_PRIVATE_KEY` and publishes `/collaboration/jwks/v1`. Dev keys are generated in `data/jwt/` by `make bootstrap`
|
|
- **Removed endpoints**: `documents/{id}/content/` (GET and PATCH), `documents/{id}/versions/`, `documents/{id}/can-edit/`. New ones: `documents/{id}/content-updated/` (called by yhub) and `documents/{id}/accesses/me/`. `documents/{id}/formatted-content/` now reads the content from yhub
|
|
- **Legacy documents**: `python manage.py migrate_documents` hands the S3 content of existing documents to yhub (resumable, idempotent); `SOFT_MIGRATION=true` on yhub seeds a document on first open. See `UPGRADE.md`
|
|
- **Search**: Full-text indexing via `manage.py index`
|
|
- **Load-test tooling**: `src/backend/loadtest/` is a separate Django application that mints sessions for existing users (`create_load_test_sessions`, `revoke_load_test_sessions`). It is only installed by the `LoadTest` configuration (`DJANGO_CONFIGURATION=LoadTest`, a subclass of `Production`); `LOAD_TEST_TOOLS_ENABLED` is `False` everywhere else, pinned in `Production`, and never read from the environment. Anything added for load tests goes there, not in `core`, and must stay off in `Production`
|
|
|
|
### Frontend
|
|
|
|
- **Editor**: BlockNote.js 0.46.x (Tiptap-based block editor)
|
|
- **Real-time**: Yjs with the plain `y-websocket` `WebsocketProvider` (`features/docs/doc-management/stores/useProviderStore.tsx`), and an HTTP polling fallback (`@y/yhub-http-fallback`) used while the websocket cannot be opened. Hocuspocus is gone
|
|
- **Offline**: documents are kept locally (`y-indexeddb`) and a service worker (`features/service-worker/`) caches pages and API answers and replays queued mutations when back online
|
|
- **Version history**: built from the activity API of yhub, not from the backend
|
|
- **State**: Zustand (broadcast store for cross-tab sync), TanStack React Query for server state
|
|
- **UI**: Mantine 8 + Cunningham design system
|
|
- **Export**: BlockNote XL packages (PDF, DOCX, ODT)
|
|
- **i18n**: i18next with Crowdin for community translations
|
|
|
|
### Collaboration Flow
|
|
|
|
Clients connect via WebSocket to the collaboration server (`src/yhub-server`, built on `@y/hub`), authenticated by their Django session cookie.
|
|
|
|
- On every connection (and permission recheck, REST call, fallback poll) yhub asks the backend who the caller is and what they may do: `GET /users/me/`, `GET /documents/{id}/`, and `GET /documents/{id}/accesses/me/` for the history. The backend stays the source of truth for access rights
|
|
- Updates travel between replicas through Redis/Valkey streams, so yhub replicas are stateless and need no sticky sessions. A worker (`YHUB_ROLE=all|server|worker`) compacts the stream into yhub's own PostgreSQL database (optionally S3 for the blobs)
|
|
- After a compaction that found new content, yhub calls `POST /documents/{id}/content-updated/` so that the backend updates `updated_at` and the search index
|
|
- The backend reads and writes content through the REST routes of yhub with an admin JWT (see `YHubService`)
|
|
|
|
## Observability
|
|
|
|
- **Metrics** (opt-in, `PROMETHEUS_METRICS_ENABLED` + a required `PROMETHEUS_API_KEY` bearer token, both services refuse to start without the key):
|
|
- backend: django-prometheus on `/metrics`, deliberately outside `/api/`, protected by `core.middleware.PrometheusAuthMiddleware`. Multiprocess mode is set up automatically for the uvicorn workers. See `documentation/metrics.md`
|
|
- yhub: `src/yhub-server/src/metrics.ts`, served on its own port (`9464`) in every role, the worker included. See the "Metrics" section of `src/yhub-server/README.md`
|
|
- chart: `ingressMetrics` publishes `/metrics`, `/metrics/yhub` and `/metrics/yhub-worker` on a dedicated host
|
|
- never put a document id, a user id or a path in a metric label
|
|
- **Sentry**: backend (`SENTRY_DSN`), y-provider, and yhub (`src/yhub-server/src/sentry.ts`, preloaded, `SENTRY_*` settings; `error`/`fatal` log lines of the shared pino logger are reported)
|
|
- **Profiling**: django-silk, opt-in with `SILK_ENABLED` (`documentation/profiling.md`)
|
|
- `documentation/load-testing.md` is the load-testing guide: tooling, setup of an instance, scenarios. Never against production
|
|
- **Load generators**: `src/loadtest/k6/` (plain k6 scripts: the page-open HTTP sequence and the heavy endpoints; every request tagged with a `name` free of identifiers, CSRF by double-submit with the frontend's `Origin`) and `src/loadtest/swarm/` (standalone TypeScript package, like `src/yhub-server`) opens many Yjs clients on yhub with the frontend's client stack, reading the manifest of `create_load_test_sessions`. Tests run against an in-process y-websocket server (`__tests__/_server.ts`); `src/loadtest/canary/` (same shape, Playwright library) drives a few real Chromium pairs through the frontend to measure page open, editor ready and keystroke propagation, with the manifest's cookies (session + `csrftoken`) set on the context
|
|
|
|
## Code Style Guidelines
|
|
|
|
### Python (Backend)
|
|
- **Formatter**: Ruff (line-length: 88)
|
|
- **Linter**: Ruff + Pylint (pylint runs only on files changed from `origin/main`)
|
|
- Imports (Ruff enforced): future, stdlib, django, third-party, impress (`core`), first-party, local
|
|
- Naming: PascalCase classes, snake_case functions/variables, UPPER_SNAKE_CASE constants
|
|
- **Print statements are forbidden** (T20 rule) - CI will reject them in backend code
|
|
- Use `@transaction.atomic`, `select_related`/`prefetch_related`, type hints `list[str]`
|
|
- Run migrations after model changes
|
|
- In `settings.py`, `post_setup` runs after django-configurations has handed the settings to Django: change lists and dicts **in place** there (`append`, `insert`, item assignment), a reassignment is silently ignored
|
|
- Use specific exceptions, no unused imports, double quotes, f-strings
|
|
- **Tests**: Prefer the `settings` fixture from `pytest-django` over `django.test.override_settings`
|
|
|
|
### TypeScript/React (Frontend)
|
|
- **Formatter**: Prettier (single quotes, trailing commas, semicolons, 80 char width)
|
|
- **Linter**: ESLint 9 with custom `eslint-plugin-docs`
|
|
- **Style**: Stylelint for CSS
|
|
- Imports: React & Next.js, libraries, internal, types, styles
|
|
- Naming: PascalCase components/types, camelCase functions, `use*` hooks, UPPER_SNAKE_CASE constants
|
|
- Strict mode, interfaces for shapes, Zustand for state, React Query for data
|
|
|
|
### TypeScript (yhub-server)
|
|
- Every environment variable is read, validated and refused at import time in `src/config.ts` (secrets through `secret()` in `src/env.ts`, which supports `NAME_FILE`): add new settings there, with a test in `__tests__/config.spec.ts`
|
|
- Prettier style with single quotes; tests are Vitest and mock `@y/hub`
|
|
- `@y/y` must stay the very copy `@y/hub` resolves (`__tests__/dependencies.spec.ts`)
|
|
|
|
## Helm/Kubernetes Deployment
|
|
|
|
### Structure
|
|
- `helmfile.yaml.gotmpl` - Helmfile defining releases and environments
|
|
- `impress/` - Helm chart with templates and default values
|
|
- `env.d/{env}/` - Environment-specific values (dev=local, feature=CI)
|
|
|
|
### Services
|
|
- Backend (Django + Celery), Frontend (Next.js), yhub (collaboration server, plus an optional separate worker with `yhub.worker.enabled`), yProvider (conversion only)
|
|
- Two standalone Valkey instances from the official `valkey/valkey` chart: `valkey-docs` (backend cache, sessions, Celery) and `valkey-yhub`
|
|
- `dev` only (`monitoring: true` in the helmfile environment): a trimmed `kube-prometheus-stack` release (`env.d/dev/values.prometheus.yaml.gotmpl`, operator + CRDs + one Prometheus at https://docs-prometheus.127.0.0.1.nip.io) scraping the backend and yhub through the `serviceMonitor` of the chart, with the token in the `docs-metrics` Secret it creates, and a Grafana at https://docs-grafana.127.0.0.1.nip.io (admin/admin) whose sidecar loads the dashboards of `src/loadtest/dashboards/` from ConfigMaps (legend `{{` escaped for helm's tpl, as the values file explains)
|
|
- Websocket ingress on `/collaboration/ws/` with no `upstream-hash-by`: any yhub replica serves any document
|
|
- DocSpec (conversion), Posthog (analytics) - optional
|
|
|
|
### Deployment Model
|
|
- Kind cluster with mkcert HTTPS, local registry (localhost:5001)
|
|
- CoreDNS for nip.io domains, dev-backend deps (Keycloak, PostgreSQL, Minio)
|
|
|
|
### Helm Guidelines
|
|
- Never commit secrets; use Got templating `{{ now | unixEpoch }}`
|
|
- Follow semantic versioning; values use `@param` annotations
|
|
|
|
## Git Conventions
|
|
|
|
### Commit Format
|
|
|
|
```
|
|
<gitmoji>(type) lowercase title
|
|
|
|
Mandatory description explaining why.
|
|
```
|
|
|
|
- **No space** between emoji and `(type)`, **one space** after closing parenthesis
|
|
- Types: `backend`, `frontend`, `ci`, `docker`, `dependencies`, `e2e`, `export`, `auth`, etc.
|
|
- Gitmoji examples: `✨` feature, `🐛` fix, `♻️` refactor, `⬆️` dependency upgrade, `🔒️` security
|
|
- Commits must be signed (`-S`) and signed-off (`--signoff`)
|
|
|
|
### Changelog
|
|
|
|
Update `CHANGELOG.md` under `[Unreleased]` with format: `- <gitmoji>(type) description` (max 80 chars per line).
|
|
|
|
### PR Checklist
|
|
|
|
Run before submitting: `make lint && make frontend-lint && make test`
|
|
|
|
## General Practices
|
|
|
|
- Commit messages should be clear and descriptive
|
|
- Run `make lint` and tests before committing
|
|
- Use DRF for REST APIs with proper serializers
|
|
- Implement proper error handling with try/except blocks
|
|
- Use Django settings for configuration
|
|
- Internationalize user-facing strings with `_()`
|
|
- Cache expensive operations, use database indexes
|
|
- Write docstrings for classes and complex functions
|
|
- Use type checking: TypeScript (frontend), mypy optional (Python)
|
|
|
|
## Environment Configuration
|
|
|
|
Config files in `env.d/development/`: `common`, `postgresql`, `kc_postgresql`, `crowdin`, `yhub`, `yhub-postgres`. The collaboration server reads its own two files and none of the backend's. Create `.local` overrides (gitignored) for custom settings.
|
|
|
|
## i18n
|
|
|
|
```bash
|
|
make i18n-generate # Extract translation strings (backend + frontend)
|
|
make i18n-compile # Compile translations for all apps
|
|
```
|
|
|
|
Internationalize user-facing strings with `_()` (backend) or i18next (frontend).
|
|
|
|
## Key Directories
|
|
|
|
- `src/backend/` - Django REST API backend
|
|
- `src/frontend/` - Next.js (apps/impress, packages/i18n, servers/y-provider)
|
|
- `src/yhub-server/` - Collaboration server (yhub)
|
|
- `src/helm/` - Helmfile and Helm chart for Kubernetes deployment
|
|
- `src/mail/` - Email template generator (MJML)
|
|
- `documentation/` - Documentation
|
|
- `.github/` - GitHub workflows and templates
|