commit 668576cdc471f209ae6591004d7ac28a838d8ead Author: Corentin JOGUET Date: Thu May 7 12:16:19 2026 +0200 chore: initial commit — formation-hub conception phase Conception complete (Phase 0) pour formation-hub Acadenice : - 19 docs Merise Agile + UML + GitOps + plans (tests/deploy/ops/api) cf docs/00-readme.md pour l'index complet - Stack Docker compose (Docmost + Baserow + Postgres + Redis + MinIO local FS) compose.yml + compose.staging.yml + compose.prod.yml - CI/CD GitHub Actions skeleton (ci, deploy-staging, deploy-prod) - Bridge service skeleton (Hono + TS + Biome + Vitest + zod + pino) - Templates GitHub : PR + 3 issue types + CODEOWNERS + dependabot.yml - Scripts ops : healthcheck, backup quotidien, smoke-test post-deploy - LICENSE AGPL-3.0 + SECURITY.md + CONTRIBUTING.md + CHANGELOG.md - Diagramme drawIO archi infra (XML importable dans diagrams.net) Decisions structurelles enregistrees : - Scope CFA + Agence avec entite PERSONNE pivot multi-roles (ADR-001) - Stack composite Docmost AGPL + Baserow MIT + bridge custom (ADR-001) - Path B : UX quasi-unified via Tiptap node-views custom (ADR-002) - Monorepo trunk-based development (ADR-003) - Postgres separe Docmost/Baserow (ADR-004) - Bridge stack Node 22 + Hono (ADR-005) - Repo neuf prefere a fork Docmost - Prod-like des le jour 1 (pas MVP) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ee1e65c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[Makefile] +indent_style = tab + +[*.{sh,bash}] +indent_size = 2 + +[*.{ts,tsx,js,jsx,json}] +indent_size = 2 + +[*.py] +indent_size = 4 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6adf189 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# formation-hub — variables d'environnement (local dev) +# Copier vers .env et remplir avec des valeurs reelles. + +# Docmost +DOCMOST_URL=http://localhost:3000 +DOCMOST_APP_SECRET=changeme-please-rotate-32-chars-minimum +DOCMOST_DB_PASSWORD=changeme + +# Baserow +BASEROW_URL=http://localhost:8080 + +# Bridge (Phase 2 — laisser vide pour l'instant) +BASEROW_API_TOKEN= +DOCMOST_API_TOKEN= diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..ec987ad --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,25 @@ +# CODEOWNERS — Acadenice formation-hub +# Regles d'auto-assignment de reviewer sur les PRs. +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +# Default owner (fallback) +* @Imugiii + +# Infra & ops +/.github/workflows/ @Imugiii +/compose*.yml @Imugiii +/Makefile @Imugiii +/scripts/ @Imugiii + +# Code custom (bridge service) +/bridge/ @Imugiii + +# Schemas Baserow +/baserow/ @Imugiii + +# Docs +/docs/ @Imugiii + +# Security-sensitive +/SECURITY.md @Imugiii +/.env.example @Imugiii diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..3dddeb5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,51 @@ +--- +name: Bug report +about: Signaler un bug +title: "[BUG] " +labels: bug +assignees: Imugiii +--- + +## Description + + + +## Etapes pour reproduire + +1. ... +2. ... +3. ... + +## Comportement attendu + + + +## Comportement observe + + + +## Environnement + +- Env (local / staging / prod) : +- Version (commit SHA ou tag) : +- Browser / device : +- OS : + +## Logs / screenshots + + + +``` + +``` + +## Severite + +- [ ] CRITICAL (service down / data loss) +- [ ] HIGH (degradation majeure) +- [ ] MEDIUM (bug fonctionnel avec workaround) +- [ ] LOW (cosmetic, edge case) + +## Notes additionnelles + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..3be09f8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,41 @@ +--- +name: Feature request +about: Proposer une nouvelle fonctionnalite +title: "[FEAT] " +labels: enhancement +--- + +## Probleme metier + + + +## Solution proposee + + + +## Alternatives considerees + + + +## Impact estime + +- Effort dev : +- Valeur metier : +- Phase visee : + +## Acceptance criteria + +```gherkin +Scenario: ... + Given ... + When ... + Then ... +``` + +## UC concerne(s) + + + +## Notes additionnelles + + diff --git a/.github/ISSUE_TEMPLATE/security.md b/.github/ISSUE_TEMPLATE/security.md new file mode 100644 index 0000000..21e5135 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security.md @@ -0,0 +1,37 @@ +--- +name: Security report (PUBLIC issue NON RECOMMANDE) +about: Pour signaler une vulnerabilite, voir SECURITY.md +title: "[SEC] " +labels: security +assignees: Imugiii +--- + +## STOP + +**Si tu signales une vulnerabilite reelle, NE PAS ouvrir une issue publique.** + +Contacte : **security@acadenice.fr** + +Voir `SECURITY.md` pour le process complet. + +--- + +## Si c'est une suggestion non-sensible (hardening, best practice) + +### Description + + + +### Risk assessment + +- CVSS score estime : +- Vector : +- Impact si exploite : + +### Recommandation + + + +### References + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..820b4ad --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,39 @@ +## Description + + + +## Type de changement + +- [ ] feat — nouvelle fonctionnalite +- [ ] fix — bug fix +- [ ] docs — documentation seulement +- [ ] refactor — refactor sans changement comportement +- [ ] test — ajout/modif tests +- [ ] chore — maintenance, deps, tooling +- [ ] ops — infra, CI/CD +- [ ] sec — security + +## Issue liee + +Closes # + +## Tests realises + +- [ ] Tests unitaires ajoutes/modifies +- [ ] Tests integration ajoutes/modifies +- [ ] Test manuel local +- [ ] Test sur staging (si applicable) + +## Checklist + +- [ ] CI vert (lint + type-check + tests + security) +- [ ] Pas de secret commit (verifier diff) +- [ ] Doc mise a jour si necessaire (`docs/`) +- [ ] CHANGELOG.md mis a jour si user-facing +- [ ] Migration data si schema change +- [ ] Compatible avec versions Docmost / Baserow pinned +- [ ] Coverage minimum respecte (80% domain, 70% global) + +## Notes pour le reviewer + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b15df59 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,60 @@ +version: 2 +updates: + # Bridge service (npm) + - package-ecosystem: "npm" + directory: "/bridge" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "Europe/Paris" + open-pull-requests-limit: 10 + versioning-strategy: "increase" + labels: + - "dependencies" + - "bridge" + commit-message: + prefix: "chore" + include: "scope" + groups: + production-dependencies: + dependency-type: "production" + development-dependencies: + dependency-type: "development" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "ops" + + # Docker compose (base images Postgres, Redis, etc.) + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + labels: + - "dependencies" + - "docker" + commit-message: + prefix: "ops" + + # Docker Bridge Dockerfile + - package-ecosystem: "docker" + directory: "/bridge" + schedule: + interval: "weekly" + day: "monday" + labels: + - "dependencies" + - "docker" + - "bridge" + commit-message: + prefix: "ops" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1dc396c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,145 @@ +name: CI + +on: + push: + branches-ignore: [main] + pull_request: + branches: [main] + +permissions: + contents: read + security-events: write + +jobs: + lint-bridge: + name: Lint bridge (Biome) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: "npm" + cache-dependency-path: bridge/package-lock.json + - run: cd bridge && npm ci + - run: cd bridge && npx biome ci . + + typecheck-bridge: + name: Type-check bridge + runs-on: ubuntu-latest + needs: lint-bridge + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: "npm" + cache-dependency-path: bridge/package-lock.json + - run: cd bridge && npm ci + - run: cd bridge && npm run typecheck + + test-bridge-unit: + name: Tests unit bridge + runs-on: ubuntu-latest + needs: typecheck-bridge + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: "npm" + cache-dependency-path: bridge/package-lock.json + - run: cd bridge && npm ci + - run: cd bridge && npm run test:unit -- --coverage + - uses: actions/upload-artifact@v4 + with: + name: coverage-unit + path: bridge/coverage/ + + test-bridge-integration: + name: Tests integration bridge + runs-on: ubuntu-latest + needs: typecheck-bridge + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: test + POSTGRES_PASSWORD: test + POSTGRES_DB: testdb + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U test" + --health-interval 5s + --health-timeout 3s + --health-retries 10 + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 5s + --health-timeout 3s + --health-retries 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: "npm" + cache-dependency-path: bridge/package-lock.json + - run: cd bridge && npm ci + - run: cd bridge && npm run test:integration + env: + DATABASE_URL: postgresql://test:test@localhost:5432/testdb + REDIS_URL: redis://localhost:6379 + + security-scan: + name: Security scan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Secret scanning (TruffleHog) + uses: trufflesecurity/trufflehog@main + with: + path: ./ + extra_args: --only-verified + - name: SAST (Semgrep) + uses: semgrep/semgrep-action@v1 + with: + config: >- + p/javascript + p/typescript + p/security-audit + p/secrets + continue-on-error: true + - name: Dep audit (npm audit) + run: cd bridge && npm audit --audit-level=high + continue-on-error: false + + docker-build: + name: Docker build + healthcheck + runs-on: ubuntu-latest + needs: [test-bridge-unit, test-bridge-integration, security-scan] + steps: + - uses: actions/checkout@v4 + - name: Build images + run: docker compose build + - name: Up stack + run: | + cp .env.example .env + docker compose up -d + - name: Wait for services + run: sleep 30 + - name: Healthcheck + run: ./scripts/healthcheck.sh + - name: Logs on failure + if: failure() + run: docker compose logs + - name: Cleanup + if: always() + run: docker compose down -v diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 0000000..143bf1a --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,81 @@ +name: Deploy Production + +on: + push: + tags: + - "v*.*.*" + workflow_dispatch: + inputs: + ref: + description: "Tag a deployer (ex: v1.2.3)" + required: true + +permissions: + contents: read + +concurrency: + group: deploy-prod + cancel-in-progress: false + +jobs: + deploy: + name: Deploy to production + runs-on: ubuntu-latest + environment: production # required reviewers configures GitHub UI + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.ref || github.ref_name }} + + - name: Validate compose configs + run: docker compose -f compose.yml -f compose.prod.yml config > /dev/null + + - name: Deploy via SSH + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.PROD_HOST }} + username: ${{ secrets.PROD_USER }} + key: ${{ secrets.PROD_SSH_KEY }} + script_stop: true + script: | + set -euo pipefail + cd /opt/formation-hub + git fetch --tags + git checkout ${{ github.event.inputs.ref || github.ref_name }} + docker compose -f compose.yml -f compose.prod.yml pull + docker compose -f compose.yml -f compose.prod.yml up -d + ./scripts/healthcheck.sh + + - name: Smoke test + run: | + set -euo pipefail + curl -fsS --max-time 10 ${{ secrets.PROD_URL }}/api/health || exit 1 + + - name: Watch logs (5 min) + run: | + # Optionnel : monitor logs apres deploy + echo "Post-deploy watch — verifier monitoring/alerts pendant 30 min" + + - name: Notify on success + if: success() + uses: slackapi/slack-github-action@v1 + with: + payload: | + { + "text": "PROD deployed: ${{ github.event.inputs.ref || github.ref_name }} — sha ${{ github.sha }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + continue-on-error: true + + - name: Notify on failure + if: failure() + uses: slackapi/slack-github-action@v1 + with: + payload: | + { + "text": "PROD deploy FAILED — ${{ github.event.inputs.ref || github.ref_name }}. Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + continue-on-error: true diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml new file mode 100644 index 0000000..b9e4eb3 --- /dev/null +++ b/.github/workflows/deploy-staging.yml @@ -0,0 +1,57 @@ +name: Deploy Staging + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: deploy-staging + cancel-in-progress: false + +jobs: + deploy: + name: Deploy to staging + runs-on: ubuntu-latest + environment: staging + steps: + - uses: actions/checkout@v4 + + - name: Validate compose configs + run: docker compose -f compose.yml -f compose.staging.yml config > /dev/null + + - name: Deploy via SSH + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.STAGING_HOST }} + username: ${{ secrets.STAGING_USER }} + key: ${{ secrets.STAGING_SSH_KEY }} + script_stop: true + script: | + set -euo pipefail + cd /opt/formation-hub + git fetch --all + git checkout ${{ github.sha }} + docker compose -f compose.yml -f compose.staging.yml pull + docker compose -f compose.yml -f compose.staging.yml up -d + ./scripts/healthcheck.sh + + - name: Smoke test + run: | + set -euo pipefail + curl -fsS --max-time 10 ${{ secrets.STAGING_URL }}/api/health || exit 1 + + - name: Notify on failure + if: failure() + uses: slackapi/slack-github-action@v1 + with: + payload: | + { + "text": "Deploy staging FAILED — sha ${{ github.sha }}\nRun: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + continue-on-error: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81a5ca2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# Secrets +.env +.env.local +.env.*.local +.env.staging +.env.prod +*.pem +*.key + +# Backups +backups/ +*.sql.gz +*.tar.gz + +# Node +node_modules/ +dist/ +build/ +.next/ +.turbo/ +.vitest/ +coverage/ +.nyc_output/ + +# Logs +*.log +logs/ +*.pid + +# OS +.DS_Store +Thumbs.db +*~ + +# Editors +.idea/ +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json.example + +# Python (si seed scripts) +__pycache__/ +*.pyc +.venv/ +venv/ + +# Misc +.cache/ +*.tmp +*.swp diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b1f4355 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog + +Toutes les modifications notables de ce projet sont documentees ici. + +Format base sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/). +Versionning suit [Semantic Versioning](https://semver.org/lang/fr/). + +## [Unreleased] + +### Added + +- Conception complete Phase 0 : 19 documents Merise Agile + UML + GitOps + plans tests/deploy/ops/api +- Stack Docker compose locale (Docmost + Baserow + Postgres + Redis) +- Makefile commandes ops (up, down, logs, backup, restore) +- Skeleton CI/CD GitHub Actions (ci, deploy-staging, deploy-prod) +- Templates PR + 3 issue types +- Documentation architecturale poussee sur Outline (collection [AGENCE] R&D Notion-Like) +- Diagramme drawIO archi infra (XML importable) + +### Decided + +- Scope etendu CFA + Agence approuve (entite PERSONNE pivot avec roles multiples) +- Stack composite Docmost (AGPL) + Baserow (MIT) + bridge custom Node TS +- Path B retenu : UX quasi-unified via Tiptap node-views custom (Phase 2) +- Repo neuf prefere a fork Docmost (decouplage propre) +- Prod-like des le jour 1 (pas MVP) + +## [0.1.0] - TBD + +### Added + +- (premier release apres Phase 1 vanilla deploy staging) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..527d6a2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,123 @@ +# Contributing + +Merci de contribuer a `formation-hub`. Ce doc resume les conventions et le workflow. + +## Code of conduct + +Etre respectueux. Critiquer le code, pas les personnes. Pas de tolerance pour le harcelement. + +## Workflow + +### 1. Setup local + +```bash +git clone git@github.com:AcadeNice/wiki.git formation-hub +cd formation-hub +cp .env.example .env +# editer .env avec tes secrets (cf SECURITY.md) +make up +``` + +### 2. Branches + +- Branche par defaut : `main` (protegee) +- Travail sur branches features : `/` +- Types : `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `ops`, `sec` +- Duree de vie max d'une branche : **3 jours** (rebase ou drop si plus vieux) + +Exemples : +- `feat/saisie-heures-mobile` +- `fix/baserow-rollup-cache` +- `docs/update-merise-mcd` + +### 3. Commits + +Format : `(): ` + +Exemples : +- `feat(bridge): add formateur mention tiptap node` +- `fix(baserow): correct rollup cache invalidation on annulation` +- `ops(ci): add SAST scan with semgrep` +- `sec(deps): bump postgres to 16.4 for CVE-2026-XXXX` + +Regles : +- **Pas d'emoji** dans les commits (regle Acadenice) +- Description en anglais, concise, imperative +- Explique le WHY si non-evident dans le body + +### 4. Pull request + +- 1 PR = 1 sujet (pas de melange feat + fix) +- Squash merge vers main (un commit propre par PR) +- Required reviews : minimum 1 approval +- CI obligatoire (lint + type-check + test + security) + +Template PR genere automatiquement (voir `.github/PULL_REQUEST_TEMPLATE.md`). + +### 5. Tests obligatoires + +Pour toute modification de `bridge/src/`, ajouter ou mettre a jour les tests : +- Unit tests : `bridge/tests/unit/` +- Integration tests : `bridge/tests/integration/` + +Coverage minimum : +- 80% sur `bridge/src/domain/` et `bridge/src/lib/` +- 70% global + +Run local : +```bash +cd bridge +npm test +npm run test:coverage +``` + +### 6. Lint et format + +Outil : **Biome** (lint + format en un) + +```bash +cd bridge +npx biome check --write . # auto-fix +npx biome ci . # verification CI +``` + +### 7. Docs + +Si ta modif change le comportement metier ou l'API : +- Mettre a jour le doc concerne dans `docs/` +- Push aussi sur Outline (cf `docs/00-readme...` pour la convention) +- Mentionner dans `CHANGELOG.md` section `[Unreleased]` + +## Quality gates (CI) + +Checks bloquants pour merge : + +- [ ] Lint Biome vert +- [ ] Type-check TypeScript vert +- [ ] Tests unitaires verts +- [ ] Tests integration verts +- [ ] Coverage minimum atteint +- [ ] Secret scanning (TruffleHog) zero hit +- [ ] SAST (Semgrep) zero `error` +- [ ] Dependency check (npm audit) zero `high`/`critical` +- [ ] Docker build OK +- [ ] Review humaine 1+ approval + +## Conventions code + +- TypeScript strict mode obligatoire +- Pas de `any` sans justification dans un commentaire +- Naming : camelCase pour vars/fonctions, PascalCase pour classes/types +- Imports : tries (Biome auto) +- Pas de console.log en prod (utiliser le logger Pino) +- Pas d'emoji dans le code, commits, ou specs (Mantra Acadenice IA-23) +- Code auto-documente, commentaires uniquement pour le POURQUOI (Mantra IA-24) + +## Methodologie + +`formation-hub` suit Merise Agile + 64 mantras BYAN. Voir `docs/04-cahier-des-charges-techniques.md` et `docs/03-decision-record.md` pour les decisions architecturales. + +## Questions + +- Ouvre une issue sur GitHub avec le label `question` +- Ou ping `@corentin` ou `@yan` directement diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f3aef9e --- /dev/null +++ b/Makefile @@ -0,0 +1,82 @@ +.PHONY: help up down restart logs ps clean reset shell-docmost shell-baserow backup backup-docmost backup-baserow status + +DATE := $(shell date +%Y%m%d-%H%M%S) +BACKUP_DIR := ./backups + +help: + @echo "formation-hub — commandes disponibles" + @echo "" + @echo "Stack lifecycle:" + @echo " make up Demarre la stack en background" + @echo " make down Stoppe la stack (conserve les volumes)" + @echo " make restart Redemarre tous les services" + @echo " make logs Suit les logs (Ctrl+C pour quitter)" + @echo " make ps Liste les services et leur etat" + @echo " make status Healthcheck rapide des endpoints HTTP" + @echo "" + @echo "Acces shell:" + @echo " make shell-docmost Shell dans le container Docmost" + @echo " make shell-baserow Shell dans le container Baserow" + @echo "" + @echo "Sauvegardes:" + @echo " make backup Backup complet (Docmost + Baserow)" + @echo " make backup-docmost Backup uniquement Docmost (pg_dump + files)" + @echo " make backup-baserow Backup uniquement Baserow (data dir)" + @echo "" + @echo "DESTRUCTIF:" + @echo " make clean Stoppe ET supprime les volumes (ATTENTION)" + @echo " make reset clean + up (reset complet)" + +up: + @test -f .env || (echo "ERREUR: .env manquant. Copier .env.example vers .env et editer." && exit 1) + docker compose up -d + @echo "" + @echo "Stack demarree :" + @echo " Docmost : $${DOCMOST_URL:-http://localhost:3000}" + @echo " Baserow : $${BASEROW_URL:-http://localhost:8080}" + +down: + docker compose down + +restart: + docker compose restart + +logs: + docker compose logs -f --tail=100 + +ps: + docker compose ps + +status: + @echo "Docmost :" && curl -sf -o /dev/null -w " HTTP %{http_code} en %{time_total}s\n" http://localhost:3000 || echo " KO" + @echo "Baserow :" && curl -sf -o /dev/null -w " HTTP %{http_code} en %{time_total}s\n" http://localhost:8080 || echo " KO" + +shell-docmost: + docker compose exec docmost sh + +shell-baserow: + docker compose exec baserow bash + +backup: backup-docmost backup-baserow + @echo "Backup complet termine dans $(BACKUP_DIR)/" + +backup-docmost: + @mkdir -p $(BACKUP_DIR) + @echo "Backup Docmost (pg_dump + files)..." + docker compose exec -T docmost-db pg_dump -U docmost docmost | gzip > $(BACKUP_DIR)/docmost-db-$(DATE).sql.gz + docker compose exec -T docmost tar czf - /app/data/storage > $(BACKUP_DIR)/docmost-files-$(DATE).tar.gz + @echo " -> $(BACKUP_DIR)/docmost-db-$(DATE).sql.gz" + @echo " -> $(BACKUP_DIR)/docmost-files-$(DATE).tar.gz" + +backup-baserow: + @mkdir -p $(BACKUP_DIR) + @echo "Backup Baserow (data dir)..." + docker compose exec -T baserow tar czf - /baserow/data > $(BACKUP_DIR)/baserow-$(DATE).tar.gz + @echo " -> $(BACKUP_DIR)/baserow-$(DATE).tar.gz" + +clean: + @echo "ATTENTION: cette commande supprime TOUS les volumes (donnees perdues)." + @read -p "Tapez 'oui' pour confirmer: " confirm; [ "$$confirm" = "oui" ] || exit 1 + docker compose down -v + +reset: clean up diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ced757 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# formation-hub + +Notion-like self-host pour **Acadenice** (CFA + Agence dev + Operations) : wiki collaboratif + bases de donnees structurees (suivi heures formation + projets clients agence + capacite par personne). + +## Stack + +| Composant | Role | License | +|-----------|------|---------| +| **Docmost** | Wiki collaboratif, spaces, share links, **diagrammes natifs** (Mermaid + Draw.io + Excalidraw depuis v0.3.0) | AGPL-3.0 | +| **Baserow** | Bases de donnees typees (relations, rollups, formules, multi-vues) | MIT (core) | +| **bridge** (Phase 2) | Service Node TS qui expose Baserow comme nodes Tiptap dans Docmost | MIT | + +## Diagrammes + +Docmost embarque nativement trois moteurs de diagrammes — zero config, zero dev : + +- **Mermaid** : diagrammes en syntaxe markdown (flowchart, sequence, ER, gantt, classe, state, journey...). Versionnable comme du code. +- **Draw.io** : editeur visuel complet pour archi technique, BPMN, infra. Stocke en SVG attachment. +- **Excalidraw** : whiteboard hand-drawn pour brainstorming, schemas pedagogiques, sketches. Stocke en SVG attachment. + +Le MCD du projet (`docs/06-merise-mcd.md`) utilise un diagramme **Mermaid ER**. Ouvre-le dans Docmost ou Outline pour le rendu visuel. + +## Etat actuel (au 2026-05-07) + +Phase 0 — Conception : +- [x] Discovery + scope etendu CFA + Agence approuve +- [x] ADR + CDC technique +- [x] Data dictionary, MCD, MLD, MCT, MOT, state diagrams, use cases, class diagram, activity diagrams +- [x] Repo structure & GitOps (CI/CD, SecOps, environnements) +- [x] Stack Docker compose locale (vanilla, sans bridge) +- [ ] MPD Baserow (table-par-table) +- [ ] Plan de tests +- [ ] Plan de deployment + CI/CD prets +- [ ] Plan d'operations + +Phase 1+ : voir `docs/04-cahier-des-charges-techniques.md` section roadmap. + +## Demarrage local + +```bash +cp .env.example .env +# editer .env avec des secrets reels +make up +``` + +- Docmost : http://localhost:3000 +- Baserow : http://localhost:8080 + +Premier lancement : creer un compte admin Docmost et Baserow via l'UI. + +## Documentation + +Numerotation **logique** : pourquoi → quoi → comment (concept) → comment (logique) → comment (physique) → comment (ops). + +| # | Doc | Theme | +|---|-----|-------| +| 01 | `docs/01-discovery-recap.md` | Pourquoi (vision/contexte) | +| 02 | `docs/02-scope-etendu-cfa-agence.md` | Quoi (perimetre approuve) | +| 03 | `docs/03-decision-record.md` | Choix structurels (ADR) | +| 04 | `docs/04-cahier-des-charges-techniques.md` | CDC technique (stack, NFR, roadmap) | +| 05 | `docs/05-data-dictionary.md` | Donnees — vocabulaire | +| 06 | `docs/06-merise-mcd.md` | Donnees — concept (ER, cardinalites) | +| 07 | `docs/07-merise-mld.md` | Donnees — logique (schema relationnel) | +| 08 | `docs/08-merise-mct.md` | Traitements — concept | +| 09 | `docs/09-merise-mot.md` | Traitements — organisation (qui/quand/outil) | +| 10 | `docs/10-state-diagrams.md` | Comportement — cycle de vie | +| 11 | `docs/11-uml-use-cases.md` | Comportement — interactions | +| 12 | `docs/12-uml-class-diagram.md` | Comportement — code OO | +| 13 | `docs/13-uml-activity-diagrams.md` | Comportement — workflows complets | +| 14 | `docs/14-repo-structure-gitops.md` | Code — arborescence + CI/CD + SecOps | +| 15 | `docs/15-baserow-mpd.md` | Implementation — Baserow concret (table par table, formules, vues) | +| 16 | `docs/16-plan-tests.md` | Qualite — pyramide tests, outils, coverage, acceptance | +| 17 | `docs/17-plan-deployment.md` | Ops — provisionnement, CI/CD detaille, releases, migrations, rollback | +| 18 | `docs/18-plan-operations.md` | Ops — monitoring, alerting, backups DR, runbooks, capacity | +| 19 | `docs/19-bridge-api-design.md` | Bridge API — endpoints, auth, webhooks, cache, integration Tiptap | + +## Methodologie + +Merise Agile + 64 mantras BYAN. Data Dictionary First, MCD/MCT cross-validation, Ockham razor sur le scope, zero emoji dans le code et les commits. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..2b6257c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,57 @@ +# Security Policy + +## Reporting a vulnerability + +Acadenice prend la securite tres au serieux. Si tu decouvres une vulnerabilite dans `formation-hub`, **ne pas l'ouvrir en issue publique**. + +Contacte directement : **security@acadenice.fr** + +Inclure : +- Description de la vulnerabilite +- Etapes pour reproduire +- Impact estime +- Version / commit SHA concerne +- Ta contact info pour la reponse coordonnee + +## Reponse + +| Etape | Delai cible | +|-------|-------------| +| Accuse de reception | < 48h ouvrees | +| Triage initial | < 5j ouvres | +| Patch developpe | depend severite (CVSS) | +| Disclosure publique | apres patch deploye en prod, embargo coordonne | + +CVE assignee si vulnerabilite serieuse, et publication sur GitHub Security Advisories. + +## Scope + +| In scope | Out of scope | +|----------|--------------| +| Code custom du bridge service (`bridge/`) | Vulnerabilites Docmost upstream (reporter chez eux) | +| Configurations infra/CI (`compose*.yml`, `.github/`) | Vulnerabilites Baserow upstream (reporter chez eux) | +| Scripts ops (`scripts/`) | Vulnerabilites Postgres/Redis/Traefik (reporter chez les vendors) | +| Schemas et formules Baserow customs (`baserow/`) | Vulnerabilites browsers / OS | + +## Classification severites (CVSS-like) + +- **CRITICAL** : RCE, data leak massive, auth bypass — patch < 24h +- **HIGH** : escalade privileges, data leak partiel — patch < 7j +- **MEDIUM** : DoS, info disclosure non-sensible — patch < 30j +- **LOW** : best-practice deviation, low-impact — next release + +## Bonnes pratiques + +Avant de signaler, verifie : +- Ton .env n'est pas commit +- Ton API token n'est pas expose en clair quelque part +- Tu as la derniere version de Docker / Docmost / Baserow + +## Hall of fame + +Liste des reporters (avec leur permission) : +- (vide pour l'instant) + +## License + +Cette politique est applicable au repo `AcadeNice/wiki`. Voir `LICENSE` pour les conditions de redistribution. diff --git a/bridge/.env.example b/bridge/.env.example new file mode 100644 index 0000000..9f8ae06 --- /dev/null +++ b/bridge/.env.example @@ -0,0 +1,31 @@ +# Bridge service — variables d'environnement +# Copier vers .env et remplir avec valeurs reelles. + +# Server +NODE_ENV=development +PORT=4000 +LOG_LEVEL=debug + +# Baserow API +BASEROW_API_URL=http://baserow:80/api +BASEROW_API_TOKEN= + +# Docmost API +DOCMOST_API_URL=http://docmost:3000/api +DOCMOST_API_TOKEN= + +# Redis (cache + idempotence webhooks) +REDIS_URL=redis://docmost-redis:6379 + +# Webhooks Baserow signature secret (HMAC-SHA256) +BASEROW_WEBHOOK_SECRET= + +# Auth tokens bridge (CSV des tokens valides + scopes — Phase 2 simple) +# Format: token1:scope1,scope2;token2:scope3 +# Phase 3 : migration vers DB dediee +BRIDGE_API_TOKENS= + +# Rate limiting (par token + endpoint) +RATE_LIMIT_READ_PER_MIN=600 +RATE_LIMIT_WRITE_PER_MIN=60 +RATE_LIMIT_WEBHOOK_PER_MIN=1000 diff --git a/bridge/.gitignore b/bridge/.gitignore new file mode 100644 index 0000000..8c92fb9 --- /dev/null +++ b/bridge/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +coverage/ +.env +.env.local +.env.*.local +*.log +.DS_Store +.vitest/ diff --git a/bridge/Dockerfile b/bridge/Dockerfile new file mode 100644 index 0000000..27e75bc --- /dev/null +++ b/bridge/Dockerfile @@ -0,0 +1,40 @@ +# Bridge service — multi-stage build +# Image finale : node 22 alpine, ~80 Mo + +ARG NODE_VERSION=22-alpine + +# --- Stage 1 : deps --- +FROM node:${NODE_VERSION} AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --only=production && npm cache clean --force + +# --- Stage 2 : build --- +FROM node:${NODE_VERSION} AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY tsconfig.json ./ +COPY src ./src +RUN npm run build + +# --- Stage 3 : runtime --- +FROM node:${NODE_VERSION} AS runtime +WORKDIR /app + +# Non-root user +RUN addgroup -g 1001 -S bridge && adduser -S bridge -u 1001 +USER bridge + +COPY --from=deps --chown=bridge:bridge /app/node_modules ./node_modules +COPY --from=build --chown=bridge:bridge /app/dist ./dist +COPY --chown=bridge:bridge package.json ./ + +ENV NODE_ENV=production +ENV PORT=4000 +EXPOSE 4000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost:4000/api/health || exit 1 + +CMD ["node", "dist/index.js"] diff --git a/bridge/biome.json b/bridge/biome.json new file mode 100644 index 0000000..b4de547 --- /dev/null +++ b/bridge/biome.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json", + "files": { + "ignoreUnknown": true, + "ignore": ["dist", "node_modules", "coverage"] + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "noUnusedImports": "error", + "noUnusedVariables": "error" + }, + "style": { + "useConst": "error", + "useTemplate": "warn" + }, + "suspicious": { + "noExplicitAny": "warn", + "noConsoleLog": "warn" + } + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100, + "lineEnding": "lf" + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "trailingCommas": "all", + "semicolons": "always", + "arrowParentheses": "always" + } + } +} diff --git a/bridge/package.json b/bridge/package.json new file mode 100644 index 0000000..d506065 --- /dev/null +++ b/bridge/package.json @@ -0,0 +1,43 @@ +{ + "name": "@acadenice/bridge", + "version": "0.1.0", + "private": true, + "description": "Bridge service Acadenice formation-hub — expose Baserow comme nodes Tiptap dans Docmost", + "license": "AGPL-3.0-or-later", + "type": "module", + "engines": { + "node": ">=22.0.0" + }, + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/index.js", + "typecheck": "tsc --noEmit", + "lint": "biome ci .", + "lint:fix": "biome check --write .", + "test": "vitest run", + "test:unit": "vitest run tests/unit", + "test:integration": "vitest run tests/integration", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" + }, + "dependencies": { + "@hono/node-server": "^1.13.0", + "decimal.js": "^10.4.3", + "dotenv": "^16.4.5", + "hono": "^4.6.0", + "ioredis": "^5.4.1", + "ofetch": "^1.4.0", + "pino": "^9.5.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.0", + "@types/node": "^22.0.0", + "@vitest/coverage-v8": "^2.1.0", + "testcontainers": "^10.13.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0", + "vitest": "^2.1.0" + } +} diff --git a/bridge/src/index.ts b/bridge/src/index.ts new file mode 100644 index 0000000..5e7b586 --- /dev/null +++ b/bridge/src/index.ts @@ -0,0 +1,32 @@ +import { serve } from '@hono/node-server'; +import { Hono } from 'hono'; +import { logger as honoLogger } from 'hono/logger'; +import { loadConfig } from './lib/config.js'; +import { logger } from './lib/logger.js'; + +const config = loadConfig(); +const app = new Hono(); + +app.use('*', honoLogger()); + +app.get('/api/health', (c) => { + return c.json({ status: 'ok', service: 'bridge', version: '0.1.0' }); +}); + +app.get('/api/ready', async (c) => { + return c.json({ status: 'ok', dependencies: { baserow: 'TODO', redis: 'TODO' } }); +}); + +app.notFound((c) => c.json({ error: { code: 'NOT_FOUND', message: 'Route not found' } }, 404)); + +app.onError((err, c) => { + logger.error({ err }, 'Unhandled error'); + return c.json( + { error: { code: 'INTERNAL', message: 'Internal server error' } }, + 500, + ); +}); + +serve({ fetch: app.fetch, port: config.port }, (info) => { + logger.info({ port: info.port, env: config.nodeEnv }, 'Bridge service started'); +}); diff --git a/bridge/src/lib/config.ts b/bridge/src/lib/config.ts new file mode 100644 index 0000000..04cde11 --- /dev/null +++ b/bridge/src/lib/config.ts @@ -0,0 +1,41 @@ +import { config as loadDotenv } from 'dotenv'; +import { z } from 'zod'; + +loadDotenv(); + +const ConfigSchema = z.object({ + nodeEnv: z.enum(['development', 'test', 'staging', 'production']).default('development'), + port: z.coerce.number().int().positive().default(4000), + logLevel: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'), + baserowApiUrl: z.string().url(), + baserowApiToken: z.string().min(1), + docmostApiUrl: z.string().url().optional(), + docmostApiToken: z.string().optional(), + redisUrl: z.string().url(), + baserowWebhookSecret: z.string().min(16, 'webhook secret must be >= 16 chars'), + bridgeApiTokens: z.string().optional(), +}); + +export type Config = z.infer; + +export function loadConfig(): Config { + const parsed = ConfigSchema.safeParse({ + nodeEnv: process.env.NODE_ENV, + port: process.env.PORT, + logLevel: process.env.LOG_LEVEL, + baserowApiUrl: process.env.BASEROW_API_URL, + baserowApiToken: process.env.BASEROW_API_TOKEN, + docmostApiUrl: process.env.DOCMOST_API_URL, + docmostApiToken: process.env.DOCMOST_API_TOKEN, + redisUrl: process.env.REDIS_URL, + baserowWebhookSecret: process.env.BASEROW_WEBHOOK_SECRET, + bridgeApiTokens: process.env.BRIDGE_API_TOKENS, + }); + + if (!parsed.success) { + const issues = parsed.error.issues.map((i) => ` - ${i.path.join('.')}: ${i.message}`).join('\n'); + throw new Error(`Invalid configuration:\n${issues}`); + } + + return parsed.data; +} diff --git a/bridge/src/lib/logger.ts b/bridge/src/lib/logger.ts new file mode 100644 index 0000000..0a979e2 --- /dev/null +++ b/bridge/src/lib/logger.ts @@ -0,0 +1,23 @@ +import pino from 'pino'; + +export const logger = pino({ + level: process.env.LOG_LEVEL ?? 'info', + transport: + process.env.NODE_ENV === 'development' + ? { + target: 'pino-pretty', + options: { colorize: true, translateTime: 'HH:MM:ss', ignore: 'pid,hostname' }, + } + : undefined, + redact: { + paths: [ + '*.password', + '*.token', + '*.secret', + '*.authorization', + 'req.headers.authorization', + 'req.headers["x-baserow-signature"]', + ], + censor: '[REDACTED]', + }, +}); diff --git a/bridge/tests/.gitkeep b/bridge/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/bridge/tsconfig.json b/bridge/tsconfig.json new file mode 100644 index 0000000..172a5cb --- /dev/null +++ b/bridge/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": false, + "sourceMap": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/compose.prod.yml b/compose.prod.yml new file mode 100644 index 0000000..0b3d8ab --- /dev/null +++ b/compose.prod.yml @@ -0,0 +1,65 @@ +# compose.prod.yml — overrides pour env production +# Usage : docker compose -f compose.yml -f compose.prod.yml up -d + +services: + docmost: + restart: always + environment: + APP_URL: ${DOCMOST_URL:?DOCMOST_URL requis sur prod} + LOG_LEVEL: warn + labels: + - "traefik.enable=true" + - "traefik.http.routers.docmost-prod.rule=Host(`wiki.acadenice.fr`)" + - "traefik.http.routers.docmost-prod.entrypoints=websecure" + - "traefik.http.routers.docmost-prod.tls.certresolver=letsencrypt" + - "traefik.http.services.docmost-prod.loadbalancer.server.port=3000" + ports: !reset [] + deploy: + resources: + limits: + memory: 2G + reservations: + memory: 512M + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + + baserow: + restart: always + environment: + BASEROW_PUBLIC_URL: ${BASEROW_URL:?BASEROW_URL requis sur prod} + labels: + - "traefik.enable=true" + - "traefik.http.routers.baserow-prod.rule=Host(`baserow.acadenice.fr`)" + - "traefik.http.routers.baserow-prod.entrypoints=websecure" + - "traefik.http.routers.baserow-prod.tls.certresolver=letsencrypt" + - "traefik.http.services.baserow-prod.loadbalancer.server.port=80" + ports: !reset [] + deploy: + resources: + limits: + memory: 3G + reservations: + memory: 1G + + docmost-db: + restart: always + deploy: + resources: + limits: + memory: 1G + + docmost-redis: + restart: always + deploy: + resources: + limits: + memory: 256M + +networks: + default: + external: true + name: traefik diff --git a/compose.staging.yml b/compose.staging.yml new file mode 100644 index 0000000..f898735 --- /dev/null +++ b/compose.staging.yml @@ -0,0 +1,44 @@ +# compose.staging.yml — overrides pour env staging +# Usage : docker compose -f compose.yml -f compose.staging.yml up -d + +services: + docmost: + restart: always + environment: + APP_URL: ${DOCMOST_URL:?DOCMOST_URL requis sur staging} + LOG_LEVEL: info + labels: + - "traefik.enable=true" + - "traefik.http.routers.docmost-staging.rule=Host(`wiki.staging.acadenice.fr`)" + - "traefik.http.routers.docmost-staging.entrypoints=websecure" + - "traefik.http.routers.docmost-staging.tls.certresolver=letsencrypt" + - "traefik.http.services.docmost-staging.loadbalancer.server.port=3000" + ports: !reset [] + + baserow: + restart: always + environment: + BASEROW_PUBLIC_URL: ${BASEROW_URL:?BASEROW_URL requis sur staging} + labels: + - "traefik.enable=true" + - "traefik.http.routers.baserow-staging.rule=Host(`baserow.staging.acadenice.fr`)" + - "traefik.http.routers.baserow-staging.entrypoints=websecure" + - "traefik.http.routers.baserow-staging.tls.certresolver=letsencrypt" + - "traefik.http.services.baserow-staging.loadbalancer.server.port=80" + ports: !reset [] + + docmost-db: + restart: always + # Sur staging, on garde un volume persiste mais on accepte un dump regulier en backup + deploy: + resources: + limits: + memory: 1G + + docmost-redis: + restart: always + +networks: + default: + external: true + name: traefik # network commun avec Traefik (a adapter selon setup) diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..3cb1fb8 --- /dev/null +++ b/compose.yml @@ -0,0 +1,76 @@ +name: formation-hub + +services: + docmost-db: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_DB: docmost + POSTGRES_USER: docmost + POSTGRES_PASSWORD: ${DOCMOST_DB_PASSWORD:?DOCMOST_DB_PASSWORD requis} + volumes: + - docmost-db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U docmost"] + interval: 5s + timeout: 3s + retries: 10 + + docmost-redis: + image: redis:7-alpine + restart: unless-stopped + volumes: + - docmost-redis:/data + command: redis-server --appendonly yes + + docmost: + image: docmost/docmost:latest + restart: unless-stopped + depends_on: + docmost-db: + condition: service_healthy + docmost-redis: + condition: service_started + environment: + APP_URL: ${DOCMOST_URL:-http://localhost:3000} + APP_SECRET: ${DOCMOST_APP_SECRET:?DOCMOST_APP_SECRET requis (32+ chars)} + DATABASE_URL: postgresql://docmost:${DOCMOST_DB_PASSWORD}@docmost-db:5432/docmost + REDIS_URL: redis://docmost-redis:6379 + STORAGE_DRIVER: local + ports: + - "3000:3000" + volumes: + - docmost-files:/app/data/storage + + baserow: + image: baserow/baserow:1.30.1 + restart: unless-stopped + environment: + BASEROW_PUBLIC_URL: ${BASEROW_URL:-http://localhost:8080} + BASEROW_BACKEND_DEBUG: "false" + BASEROW_EMAIL_SMTP: "" + ports: + - "8080:80" + volumes: + - baserow-data:/baserow/data + + # bridge: + # build: ./bridge + # restart: unless-stopped + # depends_on: + # - baserow + # - docmost-redis + # environment: + # BASEROW_API_URL: http://baserow:80/api + # BASEROW_API_TOKEN: ${BASEROW_API_TOKEN} + # DOCMOST_API_URL: http://docmost:3000/api + # DOCMOST_API_TOKEN: ${DOCMOST_API_TOKEN} + # REDIS_URL: redis://docmost-redis:6379 + # ports: + # - "4000:4000" + +volumes: + docmost-db: + docmost-redis: + docmost-files: + baserow-data: diff --git a/docs/01-discovery-recap.md b/docs/01-discovery-recap.md new file mode 100644 index 0000000..3187dbb --- /dev/null +++ b/docs/01-discovery-recap.md @@ -0,0 +1,86 @@ +# Discovery — Recap + +> Synthese de la phase de recherche, projets evalues, blocages identifies, decision finale. +> Date : 2026-05-07 + +## Contexte metier + +- Centre de formation, ~20 employes (admin + formateurs) +- Acces clients ponctuel par lien partage +- Etudiants avec espaces personnels libres +- Cible : 90-100 utilisateurs total, ~30 simultanes peak + +## Besoin fonctionnel + +1. Wiki collaboratif (SOPs, supports formation, doc interne) +2. Bases structurees liees pour le suivi des heures de formation : + - Formations (programmes complets) + - Blocs (blocs de competences) + - Modules (lecons individuelles) + - Formateurs (avec capacite annuelle) +3. Calculs automatiques d'heures attribuees / restantes par formation, par bloc, par module, par formateur +4. Bidirec backlinks dans le wiki +5. Editeur dual-mode (WYSIWYG + raw markdown a la Alexandrie Hub) +6. Self-host obligatoire, illimite users, AGPL/MIT acceptable + +## Projets OSS evalues + +### Elimines + +| Projet | Raison | +|--------|--------| +| Notion (cloud) | Pas self-host, prix au seat | +| AFFiNE | Self-host limite a 10 seats, Team License $10/seat/mois = ~2200€/an pour 20 users | +| AppFlowy | Self-host limite a 1 user + 3 guests free | +| Outline (getoutline) | Pas de bidirec backlinks reel + license BSL (restrictions commerciales) | +| SiYuan | Excellent dual-mode + bidirec mais conçu single-user, refactor team trop lourd | +| TriliumNext | Single-user origine | +| Logseq | Outliner Roam-like, stack Clojure rare, DB version en beta | +| Anytype | License "Any Source Available" non-OSI, fork commercial = zone grise | +| HedgeDoc / BookStack | Pas de bidirec, pas de DBs | +| Alexandrie Hub | Dev solo, bus factor de 1, pas viable pour boite. Sert de **reference UX** dual-mode. | + +Sources verifiees : +- [AFFiNE 10-seat limit](https://docs.affine.pro/self-host-affine/features/basic-user-quota) +- [AppFlowy 1-user limit](https://github.com/AppFlowy-IO/AppFlowy-Cloud/issues/1570) +- [Docmost AGPL pricing](https://docmost.com/pricing) +- [Outline backlinks doc](https://docs.getoutline.com/s/guide/doc/backlinks-f9YSmlNSkr) + +### Retenus + +| Projet | Role | Pourquoi | +|--------|------|----------| +| **Docmost** | Wiki collaboratif | AGPL, users illimites self-host, team workspaces + spaces + share links natifs, stack TS/NestJS/React/Tiptap mainstream, ultra-actif (release fin avril 2026) | +| **Baserow** | DBs structurees | MIT core, users illimites self-host, multi-vues (table/kanban/calendar/timeline/gallery), formules, rollups, relations, real-time collab | + +## Path retenu — Path B + +Le user a confirme le 2026-05-07 : **on ne reinvente pas la roue**, on utilise Docmost + Baserow tels quels et on construit un **bridge custom** (Node TS) qui : + +- Expose l'API Baserow comme nodes Tiptap inline dans Docmost +- Fournit des routes de rendering `/formateur/:id` et similaires comme pages Docmost-style +- Cache Redis pour eviter de spam Baserow API + +L'utilisateur final ne doit pas voir l'UI Baserow. Cliquer sur un formateur depuis le wiki = arriver sur une page Docmost qui contient les proprietes (capacite, heures restantes) en haut + zone wiki rich content en bas. + +## Profil dev + +- AdminSys + DevOps solo (Docker + Traefik + scripts ops) +- Apprend React + Tiptap au besoin (courbe d'apprentissage acceptee) +- Possibilite freelance ponctuel pour Tiptap node-views (~2 jours pair-programming) + +## Manques connus de Docmost + +| Manque | Source | Cout dev estime | +|--------|--------|-----------------| +| Bidirec backlinks | [issue #1122](https://github.com/docmost/docmost/issues/1122) | 2-4 semaines | +| Dual-mode editor (WYSIWYG ↔ raw MD) | aucune doc | 2-3 semaines | +| Guest sharing fin (Notion-style) | [discussion #1586](https://github.com/docmost/docmost/discussions/1586) | 1-2 semaines | +| DBs Notion-style integrees | non roadmap | **delegue a Baserow + bridge** | + +## Ordre d'attaque + +1. Phase 1 — Stack vanilla locale, schema Baserow, MCD/MCT documentes +2. Phase 1 bis — Deploiement staging avec Traefik, CI/CD GitHub Actions +3. Phase 2 — Bridge Node TS, premier Tiptap node-view custom, route `/formateur/:id` +4. Phase 3 — Bidirec backlinks Docmost, dual-mode editor (selon douleur reelle) diff --git a/docs/02-scope-etendu-cfa-agence.md b/docs/02-scope-etendu-cfa-agence.md new file mode 100644 index 0000000..d38cedc --- /dev/null +++ b/docs/02-scope-etendu-cfa-agence.md @@ -0,0 +1,207 @@ +# Scope etendu — CFA + Agence + Internes + +> **APPROVED le 2026-05-07 par Corentin** — Option B retenue (CFA + Agence via PERSONNE pivot). +> **Etudiants : non modelises en Baserow**, juste users Docmost avec spaces libres (heritent des templates et integrations qu'on construit). +> Refacto des docs Merise effectue dans la foulee. +> Date : 2026-05-07. +> Source : Document Fondateur Acadenice (KxvipVcxNV) lu sur le wiki. + +## 1. Constat + +Mon modele initial couvre uniquement le **CFA** (formations / blocs / modules / formateurs / heures). C'est partiel. + +Acadenice est en realite **trois activites coordonnees** : + +| Activite | Description | Entites cles | +|----------|-------------|--------------| +| **CFA** (Centre de Formation des Apprentis) | Formation : dev, graphisme, marketing, IoT, cybersec. Max 15 etudiants/classe. | FORMATION, BLOC, MODULE, FORMATEUR, ETUDIANT, INSCRIPTION | +| **AGENCE dev** | Developpement de projets pour clients reels. Les formateurs y bossent en parallele de leurs cours. | CLIENT, PROJET, TACHE, SITE_WEB, SERVEUR, INTERVENTION | +| **OPERATIONS internes** | RH, comm, batiment, evenements, vision strategique. | SALARIE, EVENEMENT, COMMUNICATION | + +**Le lien clef** : un FORMATEUR est souvent aussi DEVELOPPEUR sur projets agence. Sa capacite annuelle se split entre les deux activites. + +C'est ce qui fait l'ADN d'Acadenice (cf doc fondateur) — pas un detail qu'on peut ignorer. + +## 2. Question structurante + +**Le projet "formation-hub" doit-il modeliser :** + +- **Option A — CFA only** (mon modele actuel) : on reste sur formation/heures formateurs. L'Agence et les Operations ont leurs propres outils (Linear/Notion/etc) ou ne sont pas modelises. +- **Option B — CFA + Agence unifies** : on ajoute le suivi projets clients. La capacite Formateur-Dev est tracee unifiee. Vue 360 d'une personne (cours + projets). +- **Option C — Toute l'organisation** : CFA + Agence + Internes. Outil ERP-leger complet. + +Mon vote : **Option B**. Justification : +- Le lien CFA-Agence est central a la vision Acadenice (cf doc fondateur, section "Le lien agence-formation"). Le modeliser = capter la vraie valeur. +- L'Option C ajoute beaucoup de scope (RH, batiment, comm) qui ne tirent pas le projet vers ses objectifs immediats. +- L'Option A laisse de la valeur sur la table — la capacite Formateur-Dev est un differentiateur metier reel. + +## 3. Extension du modele propose (Option B) + +### 3.1 Nouvelle entite pivot : PERSONNE + +```mermaid +classDiagram + class Personne { + +int id + +string nom + +string prenom + +Email email + +decimal capacite_annuelle_totale + +decimal split_formation_pct + +decimal split_agence_pct + +Statut statut + } + class RoleFormateur { + +decimal heures_attribuees_formation + +decimal heures_restantes_formation + } + class RoleDeveloppeur { + +decimal heures_attribuees_agence + +decimal heures_restantes_agence + } + class RoleAdmin { + +permissions[] + } + class RoleEtudiant { + +Date date_inscription + +Formation formation_courante + } + + Personne "1" --> "0..*" RoleFormateur + Personne "1" --> "0..*" RoleDeveloppeur + Personne "1" --> "0..*" RoleAdmin + Personne "1" --> "0..*" RoleEtudiant +``` + +Une PERSONNE peut cumuler plusieurs roles. La capacite annuelle est splittee entre formation et agence selon un pourcentage configurable. + +### 3.2 Nouvelles entites Agence + +```mermaid +erDiagram + CLIENT ||--o{ PROJET : "1,N a" + PROJET ||--o{ TACHE : "1,N comporte" + TACHE ||--o{ INTERVENTION : "0,N realisee via" + PERSONNE ||--o{ INTERVENTION : "0,N realise" + PROJET }o--o{ SITE_WEB : "0,N livre" + PROJET }o--o{ SERVEUR : "0,N deploie" + + CLIENT { + int client_id PK + string client_nom UNIQUE + string client_contact_email + string client_telephone + text client_notes + enum client_statut "prospect|actif|inactif|archive" + } + PROJET { + int projet_id PK + int projet_client_id FK + string projet_nom + decimal projet_charge_heures + decimal projet_heures_realisees "rollup" + date projet_date_debut + date projet_date_fin_prevue + enum projet_statut "devis|en_cours|livre|cloture|abandonne" + } + TACHE { + int tache_id PK + int tache_projet_id FK + string tache_titre + decimal tache_charge_heures + decimal tache_heures_realisees "rollup" + enum tache_statut "todo|in_progress|done|abandoned" + } + INTERVENTION { + int intervention_id PK + int intervention_tache_id FK + int intervention_personne_id FK + decimal intervention_heures + date intervention_date + text intervention_notes + } +``` + +### 3.3 Modele global combine (CFA + Agence + Personne pivot) + +```mermaid +erDiagram + PERSONNE ||--o{ ATTRIBUTION : "FORMATEUR : enseigne" + PERSONNE ||--o{ INTERVENTION : "DEVELOPPEUR : realise" + PERSONNE ||--o{ INSCRIPTION : "ETUDIANT : suit" + + FORMATION ||--o{ BLOC : "contient" + BLOC ||--o{ MODULE : "comprend" + MODULE ||--o{ ATTRIBUTION : "attribue" + + CLIENT ||--o{ PROJET : "a" + PROJET ||--o{ TACHE : "comporte" + TACHE ||--o{ INTERVENTION : "realisee" + + FORMATION ||--o{ INSCRIPTION : "regroupe etudiants" + + PROJET }o--o{ FORMATION : "lien optionnel : projet pedagogique" +``` + +Le **lien optionnel PROJET ↔ FORMATION** capte le cas Acadenice ou des etudiants travaillent sur de vrais projets clients comme exercice pedagogique. + +### 3.4 Capacite cumulee Personne + +Pour une PERSONNE qui a plusieurs roles : + +``` +Personne.heures_attribuees_total = + SUM(ATTRIBUTION.heures_attribuees) WHERE attribution_personne_id = personne_id + + SUM(INTERVENTION.heures) WHERE intervention_personne_id = personne_id + +Personne.heures_restantes = + Personne.capacite_annuelle_totale - Personne.heures_attribuees_total +``` + +L'admin voit en un coup d'oeil sur la fiche d'un formateur-dev : +- Cours attribues : 400h +- Projets attribues : 600h +- Capacite totale : 1500h +- Restant : 500h + +## 4. Impact sur les docs existants + +| Doc | Impact | Effort refacto | +|-----|--------|---------------| +| 01 - Discovery | Mise a jour scope | 15 min | +| 02 - Decision Records | Ajouter ADR-006 sur l'extension scope | 15 min | +| 03 - Data Dictionary | Ajouter PERSONNE, CLIENT, PROJET, TACHE, INTERVENTION, ETUDIANT, INSCRIPTION | 1h | +| 04 - MCD | Etendre ER diagram, ajouter cardinalites | 30 min | +| 05 - MLD | Ajouter tables + FK | 30 min | +| 06 - UML Use Cases | Ajouter acteurs Developpeur, Client, et leurs UC | 30 min | +| 07 - State Diagrams | Ajouter cycle CLIENT, PROJET, TACHE | 30 min | +| 08 - MCT | Ajouter operations Agence | 1h | +| 09 - MOT | Ajouter ligne pour les ops Agence | 30 min | +| 10 - Class Diagram | Ajouter classes Personne, Client, Projet, Tache, Intervention | 30 min | +| 11 - Activity Diagrams | Detailler AD-04 et AD-05 | 30 min | + +**Effort refacto total : ~6-7h** si Option B validee. + +## 5. Impact sur l'implementation + +Stack reste identique (Docmost + Baserow + bridge). Mais : + +- Plus de tables Baserow (12 au lieu de 5) +- Bridge service plus important (gere la vue unifiee Personne) +- UI : tableaux de bord par personne (cours + projets en parallele) + +Capacite estime ajout dev sur Phase 2 : **+2-3 semaines** par rapport au scope CFA-only. + +## 6. Question pour validation + +**Tu confirmes Option B ?** + +- [ ] Option A — CFA only (mon modele actuel reste) +- [ ] **Option B — CFA + Agence (extension via PERSONNE pivot)** — ma recommandation +- [ ] Option C — Tout (CFA + Agence + Operations) +- [ ] Autre — tu precises + +Si **Option B** : je refacto les docs (effort ~6-7h sur 1 session) et on continue sur le scope etendu. +Si **Option A** : on reste sur le CFA, et tu modelises l'Agence dans un autre projet plus tard. + +Une autre validation a faire : **les "Etudiants" doivent-ils avoir leur propre entite dans Baserow** (avec inscription a une formation, suivi pedagogique, etc.) **ou rester juste des spaces Docmost** sans modelisation structuree ? C'est independant de A/B/C — choix metier orthogonal. diff --git a/docs/03-decision-record.md b/docs/03-decision-record.md new file mode 100644 index 0000000..513a76d --- /dev/null +++ b/docs/03-decision-record.md @@ -0,0 +1,141 @@ +# ADR — Architecture Decision Records + +Format Architecture Decision Record. Une entree par decision structurelle. + +--- + +## ADR-001 — Stack composite : Docmost (wiki) + Baserow (DBs) + +**Statut** : Accepte le 2026-05-07 +**Decideurs** : Le user (AdminSys/DevOps) + BYAN (consulting) + +### Contexte + +Aucun projet OSS unique ne couvre : +- Users illimites self-host (out : AFFiNE 10-seat, AppFlowy 1-user) +- Bidirec + DBs multi-vues + team workspaces simultanement +- Budget recurrent zero (out : AFFiNE Team License) + +### Decision + +Combiner deux outils OSS matures : +- **Docmost** pour le wiki collaboratif (AGPL, users illimites, team workspaces, share links natifs) +- **Baserow** pour les DBs structurees (MIT core, users illimites, vues multiples, rollups, relations) + +Et construire un **bridge service** custom Node TS pour unifier l'UX de surface (l'UI Baserow reste cachee cote utilisateur final). + +### Alternatives ecartees + +1. **AFFiNE Team License paid** : ~2200€/an recurrent, lock-in vendor. +2. **Custom DB engine sur Docmost** : 4-6 mois dev (~30-60k€), reinvention de la roue. +3. **Notion (cloud)** : pas self-host. +4. **Outline + add bidirec custom** : license BSL bloque usage commercial. +5. **AppFlowy paid** : disqualifie pour stack Flutter (recrutement difficile, pas web-first). + +### Consequences + +**Positives** +- Zero recurrent licensing cost (juste l'infra ~30€/mois VPS) +- Stack mainstream TS/NestJS/React/Tiptap pour Docmost et Python/Django pour Baserow +- Decouplage permet de remplacer Docmost ou Baserow sans tout refaire +- Baserow couvre les besoins DBs du cas (multi-vues, relations, rollups, formules) parmi les Airtable-likes OSS evalues + +**Negatives** +- Deux services a maintenir au lieu d'un +- Bridge service = code custom a ecrire et maintenir +- Performance : appels HTTP entre Docmost et Baserow (mitige par cache Redis) +- Risque scope creep sur le bridge (tentation de reproduire 100% de Notion) + +**A surveiller** +- Si Docmost ajoute des DBs natives (pas dans roadmap actuelle), reevaluer la valeur de Baserow +- Si AFFiNE OSS leve sa limite 10-seat, reevaluer le path A + +--- + +## ADR-002 — Path B (UX quasi-unified) plutot que Path A (deux mondes) + +**Statut** : Accepte le 2026-05-07 + +### Contexte + +Path A (cross-link URL externe entre Docmost et Baserow) demande **2-4 semaines** mais expose l'UI Baserow aux utilisateurs (jonglage entre deux applis). + +Path B (Tiptap nodes custom + API Baserow) demande **2-3 mois pour un fullstack senior, 4-6 mois pour AdminSys/DevOps solo** mais offre une UX unifiee. + +### Decision + +**Path B** retenu. Le user accepte la courbe d'apprentissage Tiptap/React et la timeline plus longue contre une UX qui ne disrupt pas l'experience metier. + +### Consequences + +- Phase 1 (Mois 1) : stack vanilla utilisable telle quelle, l'equipe peut commencer a alimenter le wiki et les DBs sans bridge +- Phase 2 (Mois 2-6) : iteration progressive sur les nodes Tiptap custom selon la douleur reelle des utilisateurs +- Risque : tentation d'aller direct au "tout brillant" sans valider l'usage. Garde-fou : Phase 1 obligatoire avant tout code custom. + +--- + +## ADR-003 — Monorepo Git + +**Statut** : Accepte le 2026-05-07 + +### Decision + +Monorepo unique `formation-hub/` versionne ensemble : +- Compose files (`compose.yml`, overrides staging/prod) +- Bridge service (`bridge/`) +- Schemas Baserow (`baserow/schemas/`) +- Patches Docmost (`docmost/patches/`) si fork phase 2+ +- Docs (`docs/`) +- CI/CD (`.github/workflows/`) + +### Pourquoi + +- Couplage fort entre infra, donnees, code custom — versionne ensemble = atomique +- Releases coordonnees (un tag = une version coherente du systeme) +- Un seul repo a cloner pour reproduire l'environnement + +### Consequences + +- Repo va grossir avec patches Docmost. Acceptable (pas de binaires lourds). +- CI/CD doit etre intelligent (ne build que ce qui a change). + +--- + +## ADR-004 — Postgres separe Docmost / Baserow + +**Statut** : Accepte le 2026-05-07 + +### Decision + +Chaque service a son propre Postgres (deux containers) en local dev. Possibilite de partager une instance physique en prod via deux databases logiques si besoin perf. + +### Pourquoi + +- Isolation : un dump de Docmost n'affecte pas Baserow +- Versions Postgres potentiellement differentes selon les exigences +- Migration upstream de Docmost ou Baserow ne risque pas de casser l'autre + +### Consequences + +- Plus de RAM consommee (~200 Mo overhead par instance) +- Plus de complexite ops (deux backups distincts) — compense par scripts Makefile + +--- + +## ADR-005 — Stack technique du bridge (Phase 2) + +**Statut** : Provisoire — sera confirme avant Phase 2 + +### Hypothese + +- **Runtime** : Node 22 LTS +- **Framework** : Hono (rapide, leger, TypeScript-first) +- **HTTP client** : ofetch ou native fetch +- **Cache** : Redis (memoire partagee avec Docmost ou dedie) +- **Validation** : zod +- **Tests** : vitest + +### A confirmer en Phase 2 + +- Si on adopte Bun runtime au lieu de Node (perf + TS native) +- Si le bridge stocke un etat propre (Postgres dedie) ou reste stateless diff --git a/docs/04-cahier-des-charges-techniques.md b/docs/04-cahier-des-charges-techniques.md new file mode 100644 index 0000000..3a9e1db --- /dev/null +++ b/docs/04-cahier-des-charges-techniques.md @@ -0,0 +1,322 @@ +# Cahier des Charges Techniques (CDC) + +> Specification technique complete : stack, choix, contraintes, NFR, architecture, roadmap. +> Version : 1.0 — Date : 2026-05-07. +> Statut : draft, a valider Yan/Ludo avant industrialisation. + +## 1. Identification + +| Champ | Valeur | +|-------|--------| +| Nom du projet | formation-hub | +| Owner technique | Corentin JOGUET (DevOps/AdminSys, bras droit Yan) | +| Validateurs | Yan (resp tech), Ludo (direction) | +| Sponsor metier | Direction Acadenice | +| Type | Outil interne — Notion-like self-host pour CFA + Agence dev | +| Date debut | 2026-05-07 | +| Date cible MVP | T+1 mois (Phase 1 — stack vanilla + setup metier) | +| Date cible v1.0 | T+3 mois (Phase 2 — bridge UX unifie) | + +## 2. Contexte metier + +Acadenice = double activite **CFA + Agence dev** (cf doc fondateur "Vision Acadenice"). Les formateurs sont aussi developpeurs sur projets clients : leur capacite annuelle se split entre les deux activites. + +Le projet formation-hub fournit l'outil interne pour : +- Wiki collaboratif (SOPs, supports, doc technique) +- Suivi heures formation (CFA) +- Suivi projets et taches Agence +- Vue 360 capacite par personne (formation + agence) +- Acces guests clients (lien partage) +- Spaces personnels etudiants (libres, non modelises) + +## 3. Objectifs + +| Objectif | Mesure | +|----------|--------| +| Centraliser la documentation | 100% des SOPs et supports formation dans l'outil | +| Tracer les heures formateurs | Saisie reguliere par 100% des formateurs | +| Tracer les heures projets clients | Saisie reguliere par 100% des devs | +| Calcul automatique heures restantes | Tableau de bord temps reel par formation, par formateur, par projet | +| Reduire le couplage outils externes | Remplacer 0 a 3 outils actuels (Excel, Trello legers ?) | +| Self-host illimite users | Aucun cout de licence par seat, juste l'infra | +| Ouvert aux etudiants | Spaces personnels libres, beneficient des templates | + +## 4. Perimetre fonctionnel + +Couvert : +- Wiki Docmost avec mermaid/drawio/excalidraw natifs +- Bases de donnees Baserow (CFA + Agence) +- Permissions hierarchiques (workspace, space, page) +- Share links avec password / expiration +- Acces guests clients + +Non couvert (out of scope v1) : +- Modelisation formelle des etudiants (inscriptions, suivi pedagogique) +- Generation factures clients +- ATS / recrutement +- Messagerie integree +- Application mobile native (UI mobile-friendly via responsive web) + +## 5. Stack technique + +### 5.1 Diagramme architecture cible + +```mermaid +flowchart TB + User([Utilisateur final
Admin/Formateur/Dev/Etudiant/Client]) + + subgraph "Edge / Reverse Proxy" + Traefik[Traefik
TLS Let's Encrypt
routing par sous-domaine] + end + + User -->|HTTPS| Traefik + Traefik -->|wiki.acadenice.fr| Docmost + Traefik -->|baserow.acadenice.fr| Baserow + Traefik -->|bridge.acadenice.fr| Bridge + + subgraph "Application services" + Docmost[Docmost
NestJS + React + Tiptap] + Baserow[Baserow
Django + Caddy interne] + Bridge[Bridge service
Node 22 + Hono
Phase 2] + end + + subgraph "Storage" + DocmostDB[(Postgres
docmost)] + DocmostRedis[(Redis
docmost)] + BaserowDB[(Postgres
baserow embedded)] + BaserowRedis[(Redis
baserow embedded)] + FS[Local FS / MinIO
docmost files] + end + + Docmost --> DocmostDB + Docmost --> DocmostRedis + Docmost --> FS + Baserow --> BaserowDB + Baserow --> BaserowRedis + Bridge -->|API REST| Baserow + Bridge --> DocmostRedis + Bridge -->|API REST| Docmost + + subgraph "Infra ops" + CronBackup[Cron host
backups quotidiens] + Monitoring[Uptime monitoring
a definir] + end + + CronBackup -->|pg_dump + tar| DocmostDB + CronBackup -->|pg_dump + tar| BaserowDB + CronBackup -->|tar| FS +``` + +### 5.2 Composants + +| Composant | Role | Techno | Version cible | License | +|-----------|------|--------|--------------|---------| +| **Docmost** | Wiki + collab + share + diagrammes natifs | NestJS, React, Tiptap, Postgres | latest stable (>= v0.8.2 mai 2026) | AGPL-3.0 | +| **Baserow** | DBs structurees + multi-vues + rollups + formules | Django, Postgres, Redis, Celery, Caddy | 1.30.x pinned | MIT (core) | +| **Bridge** (Phase 2) | API entre Docmost (Tiptap nodes custom) et Baserow | Node 22, Hono, zod, ofetch | 1.0 (a developper) | MIT (interne) | +| **PostgreSQL** | DB pour Docmost + DB pour Baserow (containers separes) | Postgres 16 alpine | 16.x | PostgreSQL License | +| **Redis** | Cache et queues (Docmost + Bridge) | Redis 7 alpine | 7.x | RSAL (free pour notre usage) | +| **MinIO** ou **local FS** | Storage attachments Docmost | MinIO | latest stable | AGPL-3.0 | +| **Traefik** | Reverse proxy, TLS auto | Traefik 3.x (deja deploye) | latest | MIT | +| **Docker / Compose** | Containerisation, orchestration | Docker 25+, compose v2 | latest | Apache 2.0 | +| **GitHub Actions** | CI/CD | GitHub Actions | — | — | + +### 5.3 Versions pinning + +- Docmost : `docmost/docmost:latest` initialement, **pinned** sur version mineure stable apres tests (ex: v0.8.2) +- Baserow : `baserow/baserow:1.30.1` (deja pinne) +- Postgres : `postgres:16-alpine` +- Redis : `redis:7-alpine` +- Node Bridge : `node:22-alpine` +- Bumps version : decision manuelle apres test staging + +## 6. Choix de stack — justifications + +| Choix | Alternatives ecartees | Raison | +|-------|-----------------------|--------| +| **Docmost** comme wiki | AFFiNE (10-seat limit), AppFlowy (1-user limit), Outline (BSL + pas de bidirec), SiYuan/Trilium (single-user), HedgeDoc (pas de DB ni bidirec) | Seul wiki AGPL avec users illimites, team workspaces, share links et diagrammes natifs (Mermaid + Draw.io + Excalidraw) integres | +| **Baserow** comme moteur DB | NocoDB (license non-OSI Sustainable Use), Teable (jeune, pas mature), Airtable (cloud paye), construire un moteur DB dans Docmost (4-6 mois) | MIT, mature, real-time collab, rollups+formules natifs, multi-vues completes, users illimites | +| **Stack composite** vs unifie | AFFiNE Team paid (~2200€/an) ou tout custom | Zero recurrent + Stack mainstream + decouple = remplaceable | +| **Bridge custom Node TS** | Embed iframe Baserow ou reecrire UI complete | UX unifie sans Tiptap reverse engineering Docmost. Effort 2-3 mois acceptable. | +| **Postgres separe** par service | Postgres partage avec 2 databases | Isolation versions, migrations independantes, dump/restore propres | +| **Hono** pour le bridge | Express, Fastify, NestJS, Bun | Leger, TypeScript-first, performance Edge-ready, simple a deployer | +| **Path B** (UX quasi-unified) vs Path A (deux mondes) | Cross-link URL externe entre Docmost et Baserow | UX unifie evite jonglage 2 onglets pour les utilisateurs | + +(Voir `02-decision-record.md` pour les ADR detailles.) + +## 7. Specifications non-fonctionnelles (NFR) + +### 7.1 Performance + +| Metrique | Cible | Mesure | +|----------|-------|--------| +| Latence saisie heures (UC-13, UCA-07) | < 2s p95 | Bridge endpoint timing | +| Latence chargement page wiki | < 1s p95 | Lighthouse | +| Recalcul rollups Baserow | < 5s | Baserow-side timing | +| Recherche full-text Docmost | < 500ms p95 | Docmost search timing | + +### 7.2 Securite + +| Aspect | Specification | +|--------|---------------| +| Auth Docmost | Email + password, SSO OIDC en option (Phase 3) | +| Auth Baserow | Email + password, JWT | +| Auth Bridge | API tokens longue duree pour service-to-service | +| TLS | Let's Encrypt via Traefik, renouvellement auto | +| Backup encryption | AES-256 sur backups distants (MinIO/S3 distant) | +| Secrets | Variables d'environnement docker-compose, fichier `.env` exclu de git | +| Audit log | Log toutes operations sensibles (suppression, archivage, partage externe) | +| RGPD | Suppression de donnees personnelles sur demande, retention etudiants/clients selon loi | + +### 7.3 Disponibilite + +| Aspect | Cible | Justification | +|--------|-------|---------------| +| Disponibilite stack | 99% (= 3.65j down/an) | Outil interne, pas critique | +| RPO (Recovery Point Objective) | 24h max | Backup quotidien | +| RTO (Recovery Time Objective) | 4h max | Restauration manuelle assistee | +| MTTR | < 1h pour bugs critiques, < 24h pour bugs mineurs | Bug critique = bloquant pour une activite | + +### 7.4 Scalabilite + +Cible v1 : ~30 users simultanes peak, 100 users total — **hors-perf** sur un VPS 4 vCPU/8 Go. + +Croissance an 5 prevue : ~150 users total, ~50 simultanes peak. **Pas de refactor stack prevu** — juste un upsizing VPS si besoin. + +### 7.5 Backup / Disaster recovery + +| Aspect | Strategie | +|--------|-----------| +| Frequence | Quotidienne, 03:00 UTC | +| Targets | Postgres docmost (pg_dump.gz), Postgres baserow embedded (pg_dump.gz), Docmost FS (tar.gz), Baserow data (tar.gz) | +| Retention | 30 jours sur disque local, 90 jours sur stockage distant (S3 / Backblaze) | +| Test restauration | Mensuel, sur un environnement test isole | +| RPO | 24h | +| RTO | 4h | + +## 8. Contraintes + +| Contrainte | Justification | +|-----------|---------------| +| Self-host obligatoire | Souverainete des donnees, pas de SaaS payant | +| Stack OSS (AGPL/MIT/Apache acceptable) | Eviter lock-in vendor | +| Docker + Compose | Stack ops existante d'Acadenice | +| Traefik reverse proxy | Stack ops existante (labels TOML) | +| GitHub pour le code | Workflow standard equipe | +| Budget recurrent : 0 (hors infra ~30€/mois) | Decision direction | +| Equipe dev : Corentin solo + freelance ponctuel pour Tiptap | Pas d'embauche dediee | + +## 9. Hypotheses / dependances + +- Le hardware serveur (VPS Hetzner ou equivalent) est commande / disponible avant deploiement staging +- Un nom de domaine `acadenice.com` ou `acadenice.fr` est disponible pour les sous-domaines wiki/baserow/bridge +- Traefik tourne deja en prod chez Acadenice — on s'integre dans le reseau Docker existant +- L'API Outline reste accessible pendant la phase de validation (push docs depuis local pour relecture) +- Docmost et Baserow continuent d'etre maintenus activement par leur upstream pendant la duree du projet + +## 10. Risques et mitigations + +| Risque | Probabilite | Impact | Mitigation | +|--------|-------------|--------|-----------| +| Docmost upstream change ses API publiques | Faible | Moyen | Pinning de versions, tests staging avant bump | +| Baserow change son data model en breaking | Moyenne | Eleve | Backups frequents, migration test avant bump | +| Tiptap node-views complexe pour AdminSys solo | **Eleve** | Moyen | Freelance senior fullstack TS pour 2-3 jours pair-programming | +| AGPL conformite (publication code source si SaaS public) | Faible | Eleve | Outil reste interne — pas SaaS public, AGPL OK | +| Charge depasse capacite VPS 4 vCPU | Faible | Moyen | Upsizing simple, cloud-native | +| Perte de cle API Outline ou Baserow | Faible | Faible | Rotation manuelle simple, secrets dans .env | +| Manque adoption metier (saisie heures non faite) | Moyenne | Eleve | Onboarding etalonne + UX simple + relances admin | + +## 11. Roadmap technique + +### Phase 0 — Conception (en cours, fini fin mai) + +- [x] Discovery + decisions +- [x] Data dictionary, MCD, MLD, UML use cases, state diagrams, MCT, MOT, class diagram, activity diagrams +- [x] CDC technique (ce doc) +- [ ] MPD Baserow (table-par-table) +- [ ] Validation metier (Yan + Ludo + admin pedagogique) + +### Phase 1 — MVP vanilla (T+1 mois) + +- [ ] Setup stack Docker compose locale → staging +- [ ] Configuration Docmost (workspace, spaces, share links) +- [ ] Configuration Baserow (4 BDDs CFA + 4 BDDs Agence) +- [ ] Migration data initiale (formations existantes, formateurs, clients, projets en cours) +- [ ] Onboarding 5-10 power users +- [ ] Backup + monitoring de base + +### Phase 2 — Bridge UX unifie (T+3 mois) + +- [ ] Bridge service skeleton + premier Tiptap node-view custom (mention `@formateur`, `@projet`) +- [ ] Routes /personne/:id, /projet/:id, /formation/:id en page Docmost-style +- [ ] Saisie heures formateur + intervention dev en bridge UI mobile-friendly +- [ ] Webhook Baserow → bridge → cache Redis pour mentions +- [ ] Migration progressive utilisateurs (formateurs + devs ouvrent le bridge plutot que Baserow direct) + +### Phase 3 — Maturite (T+6 mois) + +- [ ] Bidirec backlinks dans Docmost (custom) +- [ ] Workflow approbation heures realisees (review admin) +- [ ] Notifications avancees (Slack/Teams) +- [ ] Rapports PDF (formation + formateur + projet) +- [ ] SSO OIDC pour gros volume users + +### Phase 4 — Optimisation & extensions (T+9 mois) + +- [ ] Modelisation etudiants si besoin metier +- [ ] Integration calendrier (iCal export) +- [ ] API publique limitee pour clients (auto-service projets) +- [ ] Multi-langue UI (EN en plus de FR) + +## 12. Estimations couts + +### Infra (recurrent) + +| Element | Cout | +|---------|------| +| VPS Hetzner CPX31 ou CCX23 (4 vCPU/8 Go) | 13-30€/mois | +| Stockage backup distant (Backblaze B2 / OVH Object Storage) | 5-10€/mois | +| Domaine + sous-domaines | 15€/an | +| **Total recurrent** | **~30€/mois = ~360€/an** | + +### Dev (one-shot) + +| Phase | Effort | Cout estime | +|-------|--------|-------------| +| Phase 0 — Conception | ~10h Corentin | Inclus salaire | +| Phase 1 — MVP vanilla | ~80h Corentin | Inclus salaire | +| Phase 2 — Bridge | ~200-300h Corentin + 2-3j freelance Tiptap | Salaire + ~2k€ freelance | +| Phase 3 — Maturite | ~150h Corentin | Inclus salaire | +| **Total externe** | | **~2-3k€** (freelance ponctuel) | + +## 13. Glossaire + +| Terme | Definition | +|-------|------------| +| CFA | Centre de Formation des Apprentis | +| RNCP | Repertoire National des Certifications Professionnelles (les blocs de competences) | +| Bloc de competences | Ensemble homogene et coherent de competences validees ensemble | +| Module | Lecon individuelle au sein d'un bloc | +| Attribution | Affectation d'un module a un formateur avec heures | +| Intervention | Travail d'un developpeur sur une tache projet client (avec heures) | +| Formation pedagogique | Cas ou un projet client sert de support de formation pour des etudiants | +| Bridge | Service intermediaire qu'on construit entre Docmost et Baserow | +| Tiptap | Editor framework utilise par Docmost (extension de ProseMirror) | +| Node-view | Composant React custom integre dans un editeur Tiptap | +| Rollup | Calcul d'agregation Baserow (sum, count, avg) sur une relation | +| Path A / Path B | Strategies d'integration Docmost-Baserow (cf ADR-002) | + +## 14. References + +- Doc fondateur Vision Acadenice : [KxvipVcxNV](https://wiki.acadenice.com/doc/vision-acadenice-document-fondateur-KxvipVcxNV) +- Discovery recap : `01-discovery-recap.md` +- Decision records : `02-decision-record.md` +- Data dictionary : `05-data-dictionary.md` +- MCD : `06-merise-mcd.md` +- MLD : `07-merise-mld.md` +- Sources externes verifiees : + - [Docmost AGPL pricing](https://docmost.com/pricing) + - [Docmost v0.3.0 release Mermaid+Drawio+Excalidraw](https://github.com/docmost/docmost/releases/tag/v0.3.0) + - [AFFiNE 10-seat limit](https://docs.affine.pro/self-host-affine/features/basic-user-quota) + - [AppFlowy 1-user limit](https://github.com/AppFlowy-IO/AppFlowy-Cloud/issues/1570) + - [Baserow vs NocoDB comparison](https://www.softr.io/blog/baserow-vs-nocodb) diff --git a/docs/05-data-dictionary.md b/docs/05-data-dictionary.md new file mode 100644 index 0000000..3b07c09 --- /dev/null +++ b/docs/05-data-dictionary.md @@ -0,0 +1,220 @@ +# Data Dictionary + +> Dictionnaire de donnees complet du domaine **CFA + Agence d'Acadenice**. +> Source de verite pour le MCD, MLD et MPD. Mantra BYAN #33 : Data Dictionary First. +> Scope B valide : entite PERSONNE pivot multi-roles (formateur, developpeur, admin). +> **Etudiants** : non modelises ici, juste users Docmost. + +## Conventions + +- Codes en `snake_case`, prefixes par mnemonique d'entite +- **Source** : `S` saisi, `C` calcule (rollup/formula), `A` automatique (timestamp/sequence) +- **Type abstrait** : independant de la techno (mapping Postgres + Baserow plus loin) +- **Nullable** : `O` oui, `N` non + +## Mapping types abstrait → Postgres → Baserow + +| Type abstrait | Postgres | Baserow | +|---------------|----------|---------| +| `INT` | `INTEGER` ou `BIGINT` (PK) | `Number` (sans decimal) | +| `DECIMAL(p,s)` | `NUMERIC(p,s)` | `Number` (avec decimal) | +| `VARCHAR(n)` | `VARCHAR(n)` | `Text` | +| `TEXT` | `TEXT` | `Long text` | +| `DATE` | `DATE` | `Date` | +| `TIMESTAMPTZ` | `TIMESTAMP WITH TIME ZONE` | `Last modified time` / `Created time` | +| `ENUM(...)` | `VARCHAR + CHECK` ou type ENUM | `Single select` | +| `MULTI_ENUM(...)` | tableau VARCHAR ou table associative | `Multiple select` | +| `EMAIL` | `VARCHAR(254) + CHECK regex` | `Email` | +| `FK` | `INTEGER + REFERENCES` | `Link to table` | + +--- + +# Section 1 — Entite pivot + +## Entite PERSONNE + +Centrale au modele. Une personne peut cumuler plusieurs roles. Sa capacite annuelle totale se split entre formation et agence. + +| Code | Designation | Type | Nullable | Default | Source | Contraintes | +|------|-------------|------|----------|---------|--------|-------------| +| `personne_id` | Identifiant | INT | N | seq | A | PK | +| `personne_nom` | Nom de famille | VARCHAR(100) | N | — | S | trim | +| `personne_prenom` | Prenom | VARCHAR(100) | N | — | S | trim | +| `personne_email` | Email pro | EMAIL | N | — | S | UNIQUE, format email | +| `personne_telephone` | Telephone | VARCHAR(20) | O | NULL | S | format E.164 si rempli | +| `personne_capacite_annuelle` | Capacite totale heures/an | DECIMAL(6,2) | N | 0 | S | `>= 0` | +| `personne_split_formation_pct` | Part allouee formation | DECIMAL(4,1) | N | 50.0 | S | `0-100`, `+ split_agence_pct = 100` | +| `personne_split_agence_pct` | Part allouee agence | DECIMAL(4,1) | N | 50.0 | S | `0-100` | +| `personne_roles` | Roles cumules | MULTI_ENUM | N | — | S | `formateur \| developpeur \| admin \| direction \| support` | +| `personne_heures_attribuees_formation` | Cumul heures formation attribuees | DECIMAL(6,2) | N | 0 | C | rollup `SUM(ATTRIBUTION.heures)` | +| `personne_heures_attribuees_agence` | Cumul heures agence attribuees | DECIMAL(6,2) | N | 0 | C | rollup `SUM(INTERVENTION.heures)` | +| `personne_heures_restantes_formation` | Capacite formation restante | DECIMAL(6,2) | N | 0 | C | formula | +| `personne_heures_restantes_agence` | Capacite agence restante | DECIMAL(6,2) | N | 0 | C | formula | +| `personne_heures_restantes_total` | Capacite totale restante | DECIMAL(6,2) | N | 0 | C | formula | +| `personne_statut` | Statut | ENUM | N | `actif` | S | `actif \| inactif` | + +**Formules cles** : + +``` +personne_heures_restantes_formation = (capacite_annuelle * split_formation_pct / 100) - heures_attribuees_formation +personne_heures_restantes_agence = (capacite_annuelle * split_agence_pct / 100) - heures_attribuees_agence +personne_heures_restantes_total = capacite_annuelle - heures_attribuees_formation - heures_attribuees_agence +``` + +**Note** : un admin pur (pas formateur ni developpeur) peut avoir `capacite_annuelle = 0` et splits a 0. + +--- + +# Section 2 — Branche CFA + +## Entite FORMATION + +| Code | Designation | Type | Nullable | Default | Source | Contraintes | +|------|-------------|------|----------|---------|--------|-------------| +| `formation_id` | Identifiant | INT | N | seq | A | PK | +| `formation_nom` | Nom | VARCHAR(200) | N | — | S | UNIQUE, trim | +| `formation_description` | Description | TEXT | O | NULL | S | — | +| `formation_filiere` | Filiere | ENUM | O | NULL | S | `dev \| graphisme \| marketing \| iot \| cybersec` | +| `formation_heures_totales` | Heures totales | DECIMAL(6,2) | N | 0 | S | `>= 0` | +| `formation_heures_attribuees` | Heures attribuees blocs | DECIMAL(6,2) | N | 0 | C | rollup | +| `formation_heures_restantes` | Restantes | DECIMAL(6,2) | N | 0 | C | formula | +| `formation_statut` | Statut | ENUM | N | `draft` | S | `draft \| actif \| termine \| archive` | +| `formation_date_debut` | Date debut | DATE | O | NULL | S | — | +| `formation_date_fin` | Date fin | DATE | O | NULL | S | `>= date_debut` | +| `formation_created_at` | Cree le | TIMESTAMPTZ | N | NOW() | A | — | +| `formation_updated_at` | Modifie le | TIMESTAMPTZ | N | NOW() | A | — | + +## Entite BLOC + +| Code | Designation | Type | Nullable | Default | Source | Contraintes | +|------|-------------|------|----------|---------|--------|-------------| +| `bloc_id` | Identifiant | INT | N | seq | A | PK | +| `bloc_formation_id` | Formation parente | INT FK | N | — | S | FK → FORMATION, CASCADE | +| `bloc_nom` | Nom | VARCHAR(200) | N | — | S | UNIQUE par formation | +| `bloc_description` | Description | TEXT | O | NULL | S | — | +| `bloc_heures_prevues` | Heures du bloc | DECIMAL(6,2) | N | 0 | S | `>= 0` | +| `bloc_heures_attribuees` | Heures modules | DECIMAL(6,2) | N | 0 | C | rollup | +| `bloc_heures_restantes` | Restantes | DECIMAL(6,2) | N | 0 | C | formula | +| `bloc_ordre` | Ordre dans formation | INT | N | 0 | S | `>= 0` | + +## Entite MODULE + +| Code | Designation | Type | Nullable | Default | Source | Contraintes | +|------|-------------|------|----------|---------|--------|-------------| +| `module_id` | Identifiant | INT | N | seq | A | PK | +| `module_bloc_id` | Bloc parent | INT FK | N | — | S | FK → BLOC, CASCADE | +| `module_nom` | Nom | VARCHAR(200) | N | — | S | trim | +| `module_description` | Description | TEXT | O | NULL | S | — | +| `module_heures_prevues` | Heures prevues | DECIMAL(5,2) | N | 0 | S | `>= 0` | +| `module_heures_attribuees` | Heures attribuees | DECIMAL(5,2) | N | 0 | C | rollup | +| `module_heures_realisees` | Heures realisees | DECIMAL(5,2) | N | 0 | C | rollup | +| `module_statut` | Cycle de vie | ENUM | N | `a_attribuer` | S | `a_attribuer \| attribue \| en_cours \| realise \| annule` | + +## Entite ATTRIBUTION (Module ↔ Personne[formateur]) + +| Code | Designation | Type | Nullable | Default | Source | Contraintes | +|------|-------------|------|----------|---------|--------|-------------| +| `attribution_id` | Identifiant | INT | N | seq | A | PK | +| `attribution_module_id` | Module attribue | INT FK | N | — | S | FK → MODULE, CASCADE | +| `attribution_personne_id` | Formateur (Personne) | INT FK | N | — | S | FK → PERSONNE, RESTRICT. Personne doit avoir role `formateur` | +| `attribution_heures_attribuees` | Heures planifiees | DECIMAL(5,2) | N | 0 | S | `> 0` | +| `attribution_heures_realisees` | Heures effectuees | DECIMAL(5,2) | N | 0 | S | `>= 0` | +| `attribution_date_debut` | Debut periode | DATE | O | NULL | S | — | +| `attribution_date_fin` | Fin periode | DATE | O | NULL | S | `>= date_debut` | +| `attribution_statut` | Statut | ENUM | N | `planifie` | S | `planifie \| en_cours \| realise \| annule` | + +--- + +# Section 3 — Branche AGENCE + +## Entite CLIENT + +| Code | Designation | Type | Nullable | Default | Source | Contraintes | +|------|-------------|------|----------|---------|--------|-------------| +| `client_id` | Identifiant | INT | N | seq | A | PK | +| `client_nom` | Nom client | VARCHAR(200) | N | — | S | UNIQUE | +| `client_contact_principal` | Contact (Nom + role) | VARCHAR(200) | O | NULL | S | — | +| `client_contact_email` | Email contact | EMAIL | O | NULL | S | format email | +| `client_contact_telephone` | Telephone | VARCHAR(20) | O | NULL | S | — | +| `client_secteur` | Secteur d'activite | VARCHAR(100) | O | NULL | S | — | +| `client_notes` | Notes libres | TEXT | O | NULL | S | — | +| `client_statut` | Statut | ENUM | N | `prospect` | S | `prospect \| actif \| inactif \| archive` | +| `client_created_at` | Cree le | TIMESTAMPTZ | N | NOW() | A | — | + +## Entite PROJET + +| Code | Designation | Type | Nullable | Default | Source | Contraintes | +|------|-------------|------|----------|---------|--------|-------------| +| `projet_id` | Identifiant | INT | N | seq | A | PK | +| `projet_client_id` | Client | INT FK | N | — | S | FK → CLIENT, RESTRICT | +| `projet_nom` | Nom projet | VARCHAR(200) | N | — | S | UNIQUE par client | +| `projet_description` | Description | TEXT | O | NULL | S | — | +| `projet_type` | Type | ENUM | O | NULL | S | `site_web \| app_mobile \| api \| infra \| audit \| support \| autre` | +| `projet_charge_heures` | Charge estimee | DECIMAL(7,2) | N | 0 | S | `>= 0` | +| `projet_heures_attribuees` | Heures attribuees taches | DECIMAL(7,2) | N | 0 | C | rollup | +| `projet_heures_realisees` | Heures realisees | DECIMAL(7,2) | N | 0 | C | rollup | +| `projet_heures_restantes` | Restantes | DECIMAL(7,2) | N | 0 | C | formula | +| `projet_date_debut` | Date debut | DATE | O | NULL | S | — | +| `projet_date_fin_prevue` | Date fin prevue | DATE | O | NULL | S | `>= date_debut` | +| `projet_date_livraison` | Date livraison effective | DATE | O | NULL | S | — | +| `projet_statut` | Statut | ENUM | N | `devis` | S | `devis \| en_cours \| livre \| cloture \| abandonne` | +| `projet_formation_id` | Formation pedagogique liee | INT FK | O | NULL | S | FK → FORMATION (lien optionnel pour projets pedagogiques) | +| `projet_url` | URL livraison | VARCHAR(500) | O | NULL | S | format URL | +| `projet_repository` | URL repo Git | VARCHAR(500) | O | NULL | S | format URL | + +## Entite TACHE + +| Code | Designation | Type | Nullable | Default | Source | Contraintes | +|------|-------------|------|----------|---------|--------|-------------| +| `tache_id` | Identifiant | INT | N | seq | A | PK | +| `tache_projet_id` | Projet parent | INT FK | N | — | S | FK → PROJET, CASCADE | +| `tache_titre` | Titre | VARCHAR(200) | N | — | S | trim | +| `tache_description` | Description | TEXT | O | NULL | S | — | +| `tache_charge_heures` | Charge estimee | DECIMAL(5,2) | N | 0 | S | `>= 0` | +| `tache_heures_realisees` | Heures realisees | DECIMAL(5,2) | N | 0 | C | rollup | +| `tache_priorite` | Priorite | ENUM | O | NULL | S | `faible \| normale \| haute \| critique` | +| `tache_statut` | Statut | ENUM | N | `todo` | S | `todo \| in_progress \| review \| done \| abandoned` | +| `tache_date_debut` | Debut prevu | DATE | O | NULL | S | — | +| `tache_date_fin_prevue` | Fin prevue | DATE | O | NULL | S | `>= date_debut` | + +## Entite INTERVENTION (Tache ↔ Personne[developpeur]) + +| Code | Designation | Type | Nullable | Default | Source | Contraintes | +|------|-------------|------|----------|---------|--------|-------------| +| `intervention_id` | Identifiant | INT | N | seq | A | PK | +| `intervention_tache_id` | Tache | INT FK | N | — | S | FK → TACHE, CASCADE | +| `intervention_personne_id` | Developpeur (Personne) | INT FK | N | — | S | FK → PERSONNE, RESTRICT. Personne doit avoir role `developpeur` | +| `intervention_heures` | Heures effectuees | DECIMAL(5,2) | N | 0 | S | `> 0` | +| `intervention_date` | Date intervention | DATE | N | TODAY | S | — | +| `intervention_notes` | Notes / commit ref | TEXT | O | NULL | S | — | +| `intervention_statut` | Statut | ENUM | N | `realise` | S | `planifie \| realise \| annule` | + +--- + +# Section 4 — Cardinalites synthetiques + +| Relation | Source | Cible | Cardinalite | +|----------|--------|-------|-------------| +| FORMATION → BLOC | (1,N) | (1,1) | une formation a au moins 1 bloc | +| BLOC → MODULE | (1,N) | (1,1) | un bloc a au moins 1 module | +| MODULE ↔ PERSONNE via ATTRIBUTION | (0,N) | (0,N) | n-n porteuse | +| CLIENT → PROJET | (0,N) | (1,1) | un client peut avoir 0+ projets | +| PROJET → TACHE | (0,N) | (1,1) | un projet peut etre vide ou avoir des taches | +| TACHE ↔ PERSONNE via INTERVENTION | (0,N) | (0,N) | n-n porteuse | +| PROJET ↔ FORMATION | (0,1) | (0,N) | lien optionnel projet pedagogique | + +# Section 5 — Volumetrie estimee (an 1 / an 5) + +| Entite | An 1 | An 5 | +|--------|------|------| +| PERSONNE | ~30 | ~80 | +| FORMATION | ~10 | ~50 | +| BLOC | ~50 | ~250 | +| MODULE | ~500 | ~2500 | +| ATTRIBUTION | ~600 | ~3000 | +| CLIENT | ~10 | ~50 | +| PROJET | ~20 | ~150 | +| TACHE | ~200 | ~2000 | +| INTERVENTION | ~2000 | ~20000 | + +Volumetrie negligeable cote performance — indexation standard sur les FK suffit. diff --git a/docs/06-merise-mcd.md b/docs/06-merise-mcd.md new file mode 100644 index 0000000..2d46881 --- /dev/null +++ b/docs/06-merise-mcd.md @@ -0,0 +1,234 @@ +# MCD — Modele Conceptuel de Donnees + +> Vue conceptuelle des entites et relations. Scope B (CFA + Agence via PERSONNE pivot). +> Dictionnaire de donnees complet : `05-data-dictionary.md`. + +## 1. Vue d'ensemble + +**8 entites organisees en 3 zones** : + +| Zone | Entites | +|------|---------| +| Pivot | PERSONNE | +| CFA | FORMATION, BLOC, MODULE + ATTRIBUTION (assoc.) | +| Agence | CLIENT, PROJET, TACHE + INTERVENTION (assoc.) | + +PERSONNE est le **pivot** entre les deux activites : la meme personne peut avoir le role `formateur` (lie a ATTRIBUTION) et `developpeur` (lie a INTERVENTION). Sa capacite annuelle est splittee entre formation et agence. + +## 2. Diagrammes entites-relations + +Le modele complet a 9 entites — afficher tous les attributs sur un seul ER produit du spaghetti. On decompose en **5 vues** : globale simplifiee + 3 zones detaillees + lien pedagogique. + +### 2.1 Vue globale (simplifiee — entites et relations seules) + +```mermaid +erDiagram + PERSONNE ||--o{ ATTRIBUTION : "formateur" + PERSONNE ||--o{ INTERVENTION : "developpeur" + FORMATION ||--o{ BLOC : "" + BLOC ||--o{ MODULE : "" + MODULE ||--o{ ATTRIBUTION : "" + CLIENT ||--o{ PROJET : "" + PROJET ||--o{ TACHE : "" + TACHE ||--o{ INTERVENTION : "" + PROJET }o--o| FORMATION : "pedagogique" +``` + +### 2.2 Zone CFA (formations + heures formateurs) + +```mermaid +erDiagram + FORMATION ||--o{ BLOC : "1,N contient" + BLOC ||--o{ MODULE : "1,N comprend" + MODULE ||--o{ ATTRIBUTION : "0,N attribuee" + PERSONNE ||--o{ ATTRIBUTION : "0,N enseigne" + + FORMATION { + int formation_id PK + string formation_nom UK + enum formation_filiere + decimal formation_heures_totales + enum formation_statut + date formation_date_debut + date formation_date_fin + } + BLOC { + int bloc_id PK + int bloc_formation_id FK + string bloc_nom + decimal bloc_heures_prevues + int bloc_ordre + } + MODULE { + int module_id PK + int module_bloc_id FK + string module_nom + decimal module_heures_prevues + enum module_statut + } + ATTRIBUTION { + int attribution_id PK + int attribution_module_id FK + int attribution_personne_id FK + decimal heures_attribuees + decimal heures_realisees + date date_debut + enum statut + } + PERSONNE { + int personne_id PK + string nom_prenom + decimal capacite_annuelle + } +``` + +### 2.3 Zone Agence (projets clients + heures devs) + +```mermaid +erDiagram + CLIENT ||--o{ PROJET : "1,N a" + PROJET ||--o{ TACHE : "0,N comporte" + TACHE ||--o{ INTERVENTION : "0,N realisee" + PERSONNE ||--o{ INTERVENTION : "0,N realise" + + CLIENT { + int client_id PK + string client_nom UK + string contact_email + enum statut + } + PROJET { + int projet_id PK + int projet_client_id FK + string projet_nom + enum type + decimal charge_heures + date date_debut + enum statut + } + TACHE { + int tache_id PK + int tache_projet_id FK + string titre + decimal charge_heures + enum priorite + enum statut + } + INTERVENTION { + int intervention_id PK + int intervention_tache_id FK + int intervention_personne_id FK + decimal heures + date intervention_date + enum statut + } + PERSONNE { + int personne_id PK + string nom_prenom + decimal capacite_annuelle + } +``` + +### 2.4 Zone Personne pivot (capacite + roles) + +```mermaid +erDiagram + PERSONNE ||--o{ ATTRIBUTION : "role formateur" + PERSONNE ||--o{ INTERVENTION : "role developpeur" + + PERSONNE { + int personne_id PK + string personne_nom + string personne_prenom + string personne_email UK + decimal capacite_annuelle + decimal split_formation_pct + decimal split_agence_pct + string roles "multi-select" + decimal heures_attribuees_formation "rollup" + decimal heures_attribuees_agence "rollup" + decimal heures_restantes_total "formula" + enum statut + } + ATTRIBUTION { + int attribution_id PK + int attribution_module_id FK + int attribution_personne_id FK + decimal heures_attribuees + } + INTERVENTION { + int intervention_id PK + int intervention_tache_id FK + int intervention_personne_id FK + decimal heures + } +``` + +### 2.5 Lien projet pedagogique (cross CFA-Agence) + +```mermaid +erDiagram + PROJET }o--o| FORMATION : "0,1 projet pedagogique" + + PROJET { + int projet_id PK + int projet_formation_id FK "nullable" + string projet_nom + } + FORMATION { + int formation_id PK + string formation_nom + } +``` + +> **Notation** : ce lien est optionnel cote PROJET (un projet peut etre purement client). Cote FORMATION, plusieurs projets peuvent etre lies a une formation pedagogique. + +## 3. Cardinalites detaillees + +| Relation | Source | Cardinalite | Cible | Cardinalite | Sens metier | +|----------|--------|-------------|-------|-------------|-------------| +| CONTIENT (CFA) | FORMATION | (1,N) | BLOC | (1,1) | une formation comprend des blocs RNCP | +| COMPREND (CFA) | BLOC | (1,N) | MODULE | (1,1) | un bloc se decompose en modules | +| ATTRIBUTION (CFA) | MODULE | (0,N) | PERSONNE | (0,N) | un module est dispense par 0-N formateurs | +| EMPLOIE (Agence) | CLIENT | (1,N) | PROJET | (1,1) | un client peut avoir des projets | +| COMPORTE (Agence) | PROJET | (0,N) | TACHE | (1,1) | un projet est decoupe en taches | +| INTERVENTION (Agence) | TACHE | (0,N) | PERSONNE | (0,N) | un dev peut intervenir sur 0-N taches | +| PROJET_PEDAGOGIQUE | PROJET | (0,1) | FORMATION | (0,N) | un projet client peut servir de support pedagogique a une formation | + +## 4. Calculs (rollups + formulas) cles + +``` +PERSONNE.heures_attribuees_formation = SUM(ATTRIBUTION.heures_attribuees) + WHERE attribution_personne_id = personne_id + AND attribution_statut != 'annule' + +PERSONNE.heures_attribuees_agence = SUM(INTERVENTION.heures) + WHERE intervention_personne_id = personne_id + AND intervention_statut != 'annule' + +PERSONNE.heures_restantes_total = capacite_annuelle + - heures_attribuees_formation + - heures_attribuees_agence + +PROJET.heures_realisees = SUM(TACHE.heures_realisees) WHERE tache_projet_id = projet_id +TACHE.heures_realisees = SUM(INTERVENTION.heures) WHERE intervention_tache_id = tache_id + +(rollups CFA inchanges depuis version precedente — voir Data Dictionary) +``` + +## 5. Regles de gestion (extension) + +- **RG-PERSONNE-01** : `personne_split_formation_pct + personne_split_agence_pct = 100` (CHECK constraint) +- **RG-PERSONNE-02** : Une attribution ne peut etre creee que si la personne a `formateur` dans ses roles. +- **RG-PERSONNE-03** : Une intervention ne peut etre creee que si la personne a `developpeur` dans ses roles. +- **RG-PERSONNE-04** : `heures_restantes_total >= 0` est un warning UI, pas un blocage (depassement possible avec justification). + +(RG CFA conserves : voir version precedente du MCD pour RG-FORMATION, RG-BLOC, RG-MODULE) + +## 6. Questions ouvertes (a valider metier) + +- [ ] Le split formation/agence est-il fixe par personne ou variable par periode (ex: trimestre 1 a 70/30, trimestre 2 a 50/50) ? +- [ ] Faut-il modeliser une notion de **session** (un module enseigne plusieurs fois a des promotions differentes) ? +- [ ] Faut-il une notion de **promotion** ou de **classe** dans le CFA ? +- [ ] Pour les projets pedagogiques (lien PROJET ↔ FORMATION), comment tracer les etudiants impliques ? (Si on ne modelise pas l'etudiant, on ne peut pas formellement le lier au projet — a discuter.) +- [ ] Workflow d'approbation des heures realisees (admin valide avant facturation ou paie) ? diff --git a/docs/07-merise-mld.md b/docs/07-merise-mld.md new file mode 100644 index 0000000..d33dd94 --- /dev/null +++ b/docs/07-merise-mld.md @@ -0,0 +1,359 @@ +# MLD — Modele Logique de Donnees + +> Traduction du MCD en schema relationnel. Scope B (CFA + Agence + PERSONNE pivot). +> Implementation Baserow concrete : `15-baserow-mpd.md` (a venir). + +## 1. Vue d'ensemble du schema relationnel + +Decoupe en sous-vues pour eviter le spaghetti d'auto-layout : globale simplifiee + 3 zones + lien pedagogique. + +### 1.1 Vue globale (PK/FK seuls) + +```mermaid +erDiagram + personne ||--o{ attribution : "RESTRICT" + personne ||--o{ intervention : "RESTRICT" + formation ||--o{ bloc : "CASCADE" + bloc ||--o{ module : "CASCADE" + module ||--o{ attribution : "CASCADE" + client ||--o{ projet : "RESTRICT" + projet ||--o{ tache : "CASCADE" + tache ||--o{ intervention : "CASCADE" + projet }o--o| formation : "SET NULL" +``` + +### 1.2 Zone CFA — schema relationnel + +```mermaid +erDiagram + formation ||--o{ bloc : "FK bloc_formation_id" + bloc ||--o{ module : "FK module_bloc_id" + module ||--o{ attribution : "FK attribution_module_id" + personne ||--o{ attribution : "FK attribution_personne_id" + + formation { + INT formation_id PK + VARCHAR formation_nom UK + ENUM formation_filiere + DECIMAL heures_totales + ENUM statut + } + bloc { + INT bloc_id PK + INT bloc_formation_id FK + VARCHAR bloc_nom + DECIMAL heures_prevues + } + module { + INT module_id PK + INT module_bloc_id FK + VARCHAR module_nom + DECIMAL heures_prevues + ENUM statut + } + attribution { + INT attribution_id PK + INT attribution_module_id FK + INT attribution_personne_id FK + DECIMAL heures_attribuees + ENUM statut + } + personne { + INT personne_id PK + VARCHAR nom_prenom + } +``` + +### 1.3 Zone Agence — schema relationnel + +```mermaid +erDiagram + client ||--o{ projet : "FK projet_client_id" + projet ||--o{ tache : "FK tache_projet_id" + tache ||--o{ intervention : "FK intervention_tache_id" + personne ||--o{ intervention : "FK intervention_personne_id" + + client { + INT client_id PK + VARCHAR client_nom UK + ENUM statut + } + projet { + INT projet_id PK + INT projet_client_id FK + VARCHAR projet_nom + ENUM type + DECIMAL charge_heures + ENUM statut + } + tache { + INT tache_id PK + INT tache_projet_id FK + VARCHAR titre + DECIMAL charge_heures + ENUM statut + } + intervention { + INT intervention_id PK + INT intervention_tache_id FK + INT intervention_personne_id FK + DECIMAL heures + DATE intervention_date + } + personne { + INT personne_id PK + VARCHAR nom_prenom + } +``` + +### 1.4 Zone Personne pivot — table & FK sortantes + +```mermaid +erDiagram + personne ||--o{ attribution : "FK personne_id" + personne ||--o{ intervention : "FK personne_id" + + personne { + INT personne_id PK + VARCHAR personne_nom + VARCHAR personne_prenom + VARCHAR personne_email UK + DECIMAL capacite_annuelle + DECIMAL split_formation_pct + DECIMAL split_agence_pct + TEXT roles "multi-select" + ENUM statut + } + attribution { + INT attribution_id PK + INT module_id FK + INT personne_id FK + DECIMAL heures_attribuees + } + intervention { + INT intervention_id PK + INT tache_id FK + INT personne_id FK + DECIMAL heures + } +``` + +### 1.5 Lien pedagogique cross-zone + +```mermaid +erDiagram + projet }o--o| formation : "FK projet_formation_id (SET NULL)" + + projet { + INT projet_id PK + INT projet_formation_id FK "nullable" + } + formation { + INT formation_id PK + VARCHAR formation_nom + } +``` + +### Vue flowchart — navigation FK + +```mermaid +flowchart LR + P[personne]:::pivot + F[formation] --> B[bloc] --> M[module] --> A[attribution] + P --> A + C[client] --> Pr[projet] --> T[tache] --> I[intervention] + P --> I + Pr -.optionnel.-> F + + classDef pivot fill:#FF825C,stroke:#333,color:#fff + classDef cfa fill:#FFB347,stroke:#333,color:#000 + classDef agence fill:#5CB3FF,stroke:#333,color:#fff + class F,B,M,A cfa + class C,Pr,T,I agence +``` + +## 2. Regles de passage MCD → MLD (rappel) + +1. Chaque entite → table. +2. Relation 1,N → la PK cote (1,1) devient FK cote (1,N). +3. Relation N,N porteuse → table associative avec PK composite (ou PK auto + UNIQUE composite). +4. Champs calcules → soit caches via triggers, soit recalcules par Baserow rollup. + +## 3. Tables — definitions DDL + +### Table `personne` + +``` +personne ( + personne_id INT PK AUTO, + personne_nom VARCHAR(100) NOT NULL, + personne_prenom VARCHAR(100) NOT NULL, + personne_email VARCHAR(254) NOT NULL UNIQUE, + personne_telephone VARCHAR(20), + personne_capacite_annuelle DECIMAL(6,2) NOT NULL DEFAULT 0, + personne_split_formation_pct DECIMAL(4,1) NOT NULL DEFAULT 50.0, + personne_split_agence_pct DECIMAL(4,1) NOT NULL DEFAULT 50.0, + personne_roles VARCHAR(200) NOT NULL, -- csv ou table N:N selon Baserow + personne_statut ENUM NOT NULL DEFAULT 'actif' +) +CHECK (personne_split_formation_pct + personne_split_agence_pct = 100) +CHECK (personne_email ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$') +INDEX idx_personne_statut ON personne(personne_statut) +``` + +### Tables CFA (formation, bloc, module, attribution) + +``` +formation ( + formation_id INT PK AUTO, + formation_nom VARCHAR(200) NOT NULL UNIQUE, + formation_description TEXT, + formation_filiere ENUM('dev','graphisme','marketing','iot','cybersec'), + formation_heures_totales DECIMAL(6,2) NOT NULL DEFAULT 0, + formation_statut ENUM('draft','actif','termine','archive') NOT NULL DEFAULT 'draft', + formation_date_debut DATE, + formation_date_fin DATE, + formation_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + formation_updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +) +CHECK (formation_date_fin >= formation_date_debut OR formation_date_fin IS NULL) + +bloc ( + bloc_id INT PK AUTO, + bloc_formation_id INT NOT NULL FK → formation(formation_id) ON DELETE CASCADE, + bloc_nom VARCHAR(200) NOT NULL, + bloc_description TEXT, + bloc_heures_prevues DECIMAL(6,2) NOT NULL DEFAULT 0, + bloc_ordre INT NOT NULL DEFAULT 0, + UNIQUE (bloc_formation_id, bloc_nom) +) +INDEX idx_bloc_formation ON bloc(bloc_formation_id) + +module ( + module_id INT PK AUTO, + module_bloc_id INT NOT NULL FK → bloc(bloc_id) ON DELETE CASCADE, + module_nom VARCHAR(200) NOT NULL, + module_description TEXT, + module_heures_prevues DECIMAL(5,2) NOT NULL DEFAULT 0, + module_statut ENUM('a_attribuer','attribue','en_cours','realise','annule') NOT NULL DEFAULT 'a_attribuer' +) +INDEX idx_module_bloc ON module(module_bloc_id) +INDEX idx_module_statut ON module(module_statut) + +attribution ( + attribution_id INT PK AUTO, + attribution_module_id INT NOT NULL FK → module(module_id) ON DELETE CASCADE, + attribution_personne_id INT NOT NULL FK → personne(personne_id) ON DELETE RESTRICT, + attribution_heures_attribuees DECIMAL(5,2) NOT NULL, + attribution_heures_realisees DECIMAL(5,2) NOT NULL DEFAULT 0, + attribution_date_debut DATE, + attribution_date_fin DATE, + attribution_statut ENUM('planifie','en_cours','realise','annule') NOT NULL DEFAULT 'planifie' +) +CHECK (attribution_heures_attribuees > 0) +CHECK (attribution_heures_realisees >= 0) +INDEX idx_attribution_module ON attribution(attribution_module_id) +INDEX idx_attribution_personne ON attribution(attribution_personne_id) +INDEX idx_attribution_statut ON attribution(attribution_statut) +UNIQUE (attribution_module_id, attribution_personne_id, attribution_date_debut) + WHERE attribution_statut != 'annule' -- index partiel +``` + +### Tables Agence (client, projet, tache, intervention) + +``` +client ( + client_id INT PK AUTO, + client_nom VARCHAR(200) NOT NULL UNIQUE, + client_contact_principal VARCHAR(200), + client_contact_email VARCHAR(254), + client_contact_telephone VARCHAR(20), + client_secteur VARCHAR(100), + client_notes TEXT, + client_statut ENUM('prospect','actif','inactif','archive') NOT NULL DEFAULT 'prospect', + client_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +) + +projet ( + projet_id INT PK AUTO, + projet_client_id INT NOT NULL FK → client(client_id) ON DELETE RESTRICT, + projet_nom VARCHAR(200) NOT NULL, + projet_description TEXT, + projet_type ENUM('site_web','app_mobile','api','infra','audit','support','autre'), + projet_charge_heures DECIMAL(7,2) NOT NULL DEFAULT 0, + projet_date_debut DATE, + projet_date_fin_prevue DATE, + projet_date_livraison DATE, + projet_statut ENUM('devis','en_cours','livre','cloture','abandonne') NOT NULL DEFAULT 'devis', + projet_formation_id INT FK → formation(formation_id) ON DELETE SET NULL, + projet_url VARCHAR(500), + projet_repository VARCHAR(500), + UNIQUE (projet_client_id, projet_nom) +) +INDEX idx_projet_client ON projet(projet_client_id) +INDEX idx_projet_statut ON projet(projet_statut) +INDEX idx_projet_formation ON projet(projet_formation_id) + +tache ( + tache_id INT PK AUTO, + tache_projet_id INT NOT NULL FK → projet(projet_id) ON DELETE CASCADE, + tache_titre VARCHAR(200) NOT NULL, + tache_description TEXT, + tache_charge_heures DECIMAL(5,2) NOT NULL DEFAULT 0, + tache_priorite ENUM('faible','normale','haute','critique'), + tache_statut ENUM('todo','in_progress','review','done','abandoned') NOT NULL DEFAULT 'todo', + tache_date_debut DATE, + tache_date_fin_prevue DATE +) +INDEX idx_tache_projet ON tache(tache_projet_id) +INDEX idx_tache_statut ON tache(tache_statut) + +intervention ( + intervention_id INT PK AUTO, + intervention_tache_id INT NOT NULL FK → tache(tache_id) ON DELETE CASCADE, + intervention_personne_id INT NOT NULL FK → personne(personne_id) ON DELETE RESTRICT, + intervention_heures DECIMAL(5,2) NOT NULL, + intervention_date DATE NOT NULL DEFAULT CURRENT_DATE, + intervention_notes TEXT, + intervention_statut ENUM('planifie','realise','annule') NOT NULL DEFAULT 'realise' +) +CHECK (intervention_heures > 0) +INDEX idx_intervention_tache ON intervention(intervention_tache_id) +INDEX idx_intervention_personne ON intervention(intervention_personne_id) +INDEX idx_intervention_date ON intervention(intervention_date) +``` + +## 4. Comportement ON DELETE + +| Relation | ON DELETE | Justification | +|----------|-----------|---------------| +| formation → bloc | CASCADE | Cycle de vie partage | +| bloc → module | CASCADE | Cycle de vie partage | +| module → attribution | CASCADE | Si module supprime, attributions deviennent orphelines | +| client → projet | RESTRICT | Empeche suppression client ayant projets | +| projet → tache | CASCADE | Cycle de vie partage | +| tache → intervention | CASCADE | Idem | +| personne → attribution / intervention | RESTRICT | Force a archiver `personne_statut = inactif` plutot que supprimer | +| projet → formation (lien pedagogique) | SET NULL | Suppression formation laisse le projet exister | + +## 5. Mapping vers Baserow + +| Concept SQL | Baserow equivalent | +|-------------|--------------------| +| Table | Database → Table | +| FK | `Link to table` (gere bidirec auto) | +| ENUM | `Single select` | +| MULTI_ENUM (personne_roles) | `Multiple select` | +| FK ON DELETE CASCADE | UI Baserow ou webhook → bridge | +| INDEX | Implicite sur `Link to table` | +| CHECK CONSTRAINT | Validation cote bridge ou formules de validation | +| Calculated rollup/formula | `Lookup` + `Formula` + `Count` | + +Voir `15-baserow-mpd.md` (a venir) pour la traduction concrete table-par-table. + +## 6. Questions ouvertes + +- [ ] Materialiser les calculs ou recalculer a la lecture ? Baserow rollups = cache materialise auto. OK pour notre volumetrie. +- [ ] Soft-delete ou hard-delete ? Si Qualiopi exige tracabilite, soft-delete obligatoire (ajouter `*_deleted_at TIMESTAMPTZ NULLABLE` partout). +- [ ] Audit log par row ? Baserow a `Last modified by` + `Last modified time` natifs. Suffisant ou il faut un journal d'evenements separe ? +- [ ] Multi-tenant futur ? Pour l'instant Acadenice mono-instance. Si rachat / scaling, ajouter `tenant_id` a toutes les tables. diff --git a/docs/08-merise-mct.md b/docs/08-merise-mct.md new file mode 100644 index 0000000..2a70c18 --- /dev/null +++ b/docs/08-merise-mct.md @@ -0,0 +1,226 @@ +# MCT — Modele Conceptuel de Traitements + +> Vue dynamique des operations metier. Complete le MCD (statique) et les state diagrams (cycles de vie). +> Methodologie : Merise. Chaque operation = evenement declencheur + regle de synchronisation + actions + evenement resultat. + +## 1. Conventions + +- **Evenement** : un fait survenant dans le systeme, declenche une operation. Note `EV-XX`. +- **Operation** : traitement metier qui transforme un etat. Note `OP-XX`. +- **Regle de synchronisation** : condition logique sur les evenements entrants (AND/OR). +- **Regle d'emission** : condition sur les evenements de sortie selon le resultat. + +## 2. Liste des evenements + +| Code | Evenement | Origine | +|------|-----------|---------| +| EV-01 | Demande creation formation | Admin | +| EV-02 | Specs blocs renseignees | Admin | +| EV-03 | Specs modules renseignees | Admin | +| EV-04 | Demande attribution module | Admin | +| EV-05 | Formateur dispo (capacite >= heures requises) | System (calcul) | +| EV-06 | Date debut module atteinte | System (cron) | +| EV-07 | Saisie heures realisees | Formateur | +| EV-08 | Date fin module atteinte | System (cron) | +| EV-09 | Demande annulation attribution | Admin | +| EV-10 | Rollup recalcule | System (auto) | +| EV-11 | Capacite formateur depassee | System (calcul) | +| EV-12 | Demande archivage formation | Admin | +| EV-13 | Tous modules d'une formation realises | System (calcul) | +| EV-14 | Demande edition page wiki | Admin / Formateur | +| EV-15 | Lien partage cree | Admin | + +## 3. Liste des operations + +| Code | Operation | Acteur principal | +|------|-----------|------------------| +| OP-01 | Creer une formation | Admin | +| OP-02 | Decomposer en blocs et modules | Admin | +| OP-03 | Attribuer un module a un formateur | Admin | +| OP-04 | Saisir heures realisees | Formateur | +| OP-05 | Annuler une attribution | Admin | +| OP-06 | Cloturer un module | System / Admin | +| OP-07 | Cloturer une formation | System / Admin | +| OP-08 | Recalculer rollups | System (auto) | +| OP-09 | Notifier depassement capacite | System (auto) | +| OP-10 | Archiver une formation | Admin | +| OP-11 | Generer rapport formation | Admin | +| OP-12 | Generer rapport formateur | Admin | +| OP-13 | Inviter client par lien partage | Admin | +| OP-14 | Backup quotidien | System (cron) | + +## 4. Operations detaillees + +### OP-01 — Creer une formation + +```mermaid +flowchart TD + EV01([EV-01 Demande creation formation]) --> SYNC1{Regle sync:
EV-01 saisi} + SYNC1 --> OP01[OP-01 Creer formation] + OP01 -->|Validation OK| EV_OUT_OK([EV: Formation creee statut=draft]) + OP01 -->|Nom deja existant| EV_OUT_ERR([EV: Erreur saisie]) +``` + +- **Entrants** : EV-01 +- **Synchro** : EV-01 saisi +- **Actions** : + 1. Valider unicite du nom + 2. Valider `heures_totales > 0` + 3. INSERT formation avec `statut = draft`, timestamps auto +- **Sortants** : + - Si OK → "Formation creee" (statut = draft) + - Si erreur → "Erreur saisie" + +### OP-03 — Attribuer un module a un formateur + +```mermaid +flowchart TD + EV04([EV-04 Demande attribution]) --> SYNC2{AND} + EV05([EV-05 Formateur dispo]) --> SYNC2 + SYNC2 --> OP03[OP-03 Creer attribution] + OP03 -->|RG-01 OK| EV_ATTR([EV: Attribution creee statut=planifie]) + OP03 -->|RG-01 KO| EV_BLOCK([EV: Erreur depassement heures module]) + EV_ATTR --> OP08_TRIG[Trigger OP-08 Recalculer rollups] +``` + +- **Entrants** : EV-04 AND EV-05 +- **Synchro** : les deux evenements presents (formateur dispo verifie avant ouverture du formulaire) +- **Actions** : + 1. Valider RG-01 : `SUM(attributions.heures) + nouvelle_attribution.heures <= module.heures_prevues` + 2. Valider RG-02 : warning si `formateur.heures_attribuees + nouvelle.heures > formateur.capacite` (mais pas blocage) + 3. INSERT attribution avec `statut = planifie` + 4. Declenche EV-10 (rollup recalcule) +- **Sortants** : + - Si OK → "Attribution creee" + EV-10 + - Si RG-01 KO → "Erreur depassement" + - Si RG-02 warning → "Attribution creee" + "Warning capacite" + +### OP-04 — Saisir heures realisees + +- **Entrants** : EV-07 (saisie formateur) +- **Synchro** : EV-07 ET attribution.statut IN (`planifie`, `en_cours`) +- **Actions** : + 1. Valider RG-05 : `heures_realisees <= heures_attribuees + tolerance` + 2. UPDATE attribution.heures_realisees + 3. Si `heures_realisees == heures_attribuees` → attribution.statut = `realise` + 4. Declenche EV-10 (rollup recalcule) +- **Sortants** : "Heures saisies" + EV-10. Si RG-05 KO → "Erreur depassement heures" + +### OP-06 — Cloturer un module + +- **Entrants** : EV-08 OR EV-13 +- **Synchro** : + - EV-08 (date fin atteinte) ET `module.statut = en_cours` + - OU manuel admin via bouton "Marquer realise" +- **Actions** : + 1. Verifier : toutes attributions du module en `realise` ou `annule` + 2. UPDATE module.statut = `realise` + 3. Si tous les modules de la formation = `realise` → declenche EV-13 +- **Sortants** : "Module cloture" + +### OP-07 — Cloturer une formation + +- **Entrants** : EV-13 (tous modules realises) ET formation.date_fin atteinte +- **Synchro** : AND des deux +- **Actions** : UPDATE formation.statut = `termine` +- **Sortants** : "Formation terminee" + +### OP-08 — Recalculer rollups + +- **Entrants** : EV-10 (toute modification de attribution, module, bloc, formation) +- **Actions** : + 1. Recalcul cascadant : attribution → module → bloc → formation + 2. Recalcul formateur.heures_attribuees / heures_restantes + 3. Si formateur.heures_restantes < 0 → declenche EV-11 +- **Sortants** : "Rollups a jour" + EV-11 si depassement + +### OP-09 — Notifier depassement capacite + +- **Entrants** : EV-11 +- **Actions** : + 1. Identifier admins du workspace + 2. Envoyer notification (email + dans-app) avec details +- **Sortants** : "Notification envoyee" + +## 5. Diagramme global du flux + +```mermaid +flowchart TD + EV01([EV-01]) --> OP01[OP-01 Creer formation] + OP01 --> EV02_PRE([Formation creee]) + + EV02_PRE --> OP02[OP-02 Decomposer en blocs/modules] + OP02 --> EV_MOD([Modules crees a_attribuer]) + + EV_MOD --> OP03[OP-03 Attribuer module] + OP03 --> EV_ATTR_OK([Attribution planifie]) + EV_ATTR_OK --> OP08[OP-08 Recalcul rollups] + + EV06([EV-06 Date debut]) --> EV_EN_COURS([Module en_cours]) + EV_EN_COURS --> OP04[OP-04 Saisie heures realisees] + OP04 --> OP08 + + OP04 --> EV_REAL([Module realise]) + EV_REAL --> OP06[OP-06 Cloturer module] + + OP06 --> EV13([EV-13 Tous modules realises]) + EV13 --> OP07[OP-07 Cloturer formation] + OP07 --> EV_TERM([Formation terminee]) + + OP08 --> EV11{Capacite
depassee?} + EV11 -->|Oui| OP09[OP-09 Notifier admin] +``` + +## 6. Operations secondaires + +| Operation | Trigger | Frequence | +|-----------|---------|-----------| +| OP-11 Generer rapport formation | Demande admin | A la demande | +| OP-12 Generer rapport formateur | Demande admin | A la demande | +| OP-13 Inviter client | Demande admin | A la demande | +| OP-14 Backup | Cron quotidien | 1x/jour | + +## 7. Operations Agence (extension scope B) + +| Code | Operation | Acteur principal | Trigger | +|------|-----------|------------------|---------| +| OPA-01 | Creer client | Admin | Manuel | +| OPA-02 | Creer projet | Admin | Devis signe | +| OPA-03 | Decomposer projet en taches | Admin | Apres creation projet | +| OPA-04 | Attribuer tache a un developpeur | Admin | Manuel | +| OPA-05 | Saisir intervention | Developpeur | Apres travail effectue | +| OPA-06 | Marquer tache `done` | Developpeur ou Admin | Validation | +| OPA-07 | Cloturer projet | Admin | Toutes taches done + livraison validee | +| OPA-08 | Recalculer rollups Agence | System | Sur evenement (intervention modifiee) | +| OPA-09 | Lier projet a formation pedagogique | Admin | Optionnel, sur fiche projet | +| OPA-10 | Recalculer capacite Personne (formation + agence) | System | Sur evenement (attribution OU intervention modifiee) | + +### OPA-05 — Saisir intervention (developpeur) + +- **Entrants** : Developpeur ouvre l'app + selectionne tache +- **Synchro** : Personne.roles contient `developpeur` ET tache.statut != `abandoned/done` +- **Actions** : + 1. Valider `intervention_heures > 0` + 2. INSERT intervention statut `realise` + 3. Declenche OPA-08 + OPA-10 (recalcul rollups) +- **Sortants** : "Intervention saisie" + "Rollups a jour" + +### OPA-10 — Recalculer capacite Personne unifiee + +C'est une evolution de l'OP-08 du MCT initial. Maintenant que Personne pivot existe : + +``` +Personne.heures_attribuees_formation = SUM(ATTRIBUTION.heures_attribuees) WHERE personne ET statut != annule +Personne.heures_attribuees_agence = SUM(INTERVENTION.heures) WHERE personne ET statut != annule +Personne.heures_restantes_total = capacite_annuelle - heures_attribuees_formation - heures_attribuees_agence +``` + +Si `heures_restantes_total < 0` → declenche notification depassement (OP-09 etendu pour capacite totale). + +## 8. Questions ouvertes (a valider en MOT) + +- [ ] OP-04 (saisie heures) : declenchee quand par le formateur ? En fin de session ? En fin de mois ? +- [ ] OP-06/OP-07 : cloture automatique a la date fin OU manuelle apres validation admin ? +- [ ] OP-09 (notification depassement) : warning soft ou blocage dur ? Email + Slack/Teams ? +- [ ] OP-11/OP-12 (rapports) : format PDF, CSV, Excel ? Push vers comptabilite ? +- [ ] OP-08 (rollups) : tolerance de delai acceptable ? Eventual consistency OK ou TR strict ? diff --git a/docs/09-merise-mot.md b/docs/09-merise-mot.md new file mode 100644 index 0000000..c2b59bb --- /dev/null +++ b/docs/09-merise-mot.md @@ -0,0 +1,186 @@ +# MOT — Modele Organisationnel de Traitements + +> Vue organisationnelle des operations : QUI fait QUOI, QUAND, COMMENT, AVEC QUEL OUTIL. +> Methodologie : Merise. Le MOT prend les operations du MCT et les "concretise" cote organisation. + +## 1. Conventions + +- **Type** : + - `M` = Manuel (saisie utilisateur) + - `A` = Automatique (declenche par evenement) + - `B` = Batch (planifie) + - `S` = Semi-auto (validation manuelle d'un calcul auto) +- **Temps** : + - `TR` = Temps reel (synchrone) + - `D` = Differe (asynchrone, < 1min) + - `B` = Batch (planifie cron) +- **Outil** : composant qui execute (Baserow UI, Bridge, Cron, Docmost, Email, etc.) + +## 2. Tableau MOT consolide + +| Code | Operation (MCT) | Acteur | Type | Temps | Outil | Notes | +|------|-----------------|--------|------|-------|-------|-------| +| OP-01 | Creer formation | Admin | M | TR | Baserow UI | Formulaire avec validation | +| OP-02 | Decomposer en blocs/modules | Admin | M | TR | Baserow UI | Vue hierarchique | +| OP-03 | Attribuer module | Admin | M | TR | Baserow UI ou bridge form | Vue kanban "a attribuer" | +| OP-04 | Saisir heures realisees | Formateur | M | TR | Bridge UI mobile-friendly | A faire en fin de session ideal | +| OP-05 | Annuler attribution | Admin | M | TR | Baserow UI | Avec champ justification obligatoire | +| OP-06 | Cloturer module | System + Admin | S | TR | Baserow auto + bouton admin | Auto si toutes attributions realisees | +| OP-07 | Cloturer formation | System | A | D | Cron horaire + bridge webhook | Auto si EV-13 + date_fin | +| OP-08 | Recalculer rollups | System | A | TR | Baserow rollup natif | Re-calc transparent | +| OP-09 | Notifier depassement | System | A | TR | Bridge → SMTP / Slack webhook | Email + canal interne | +| OP-10 | Archiver formation | Admin | M | TR | Baserow UI | Action sensible, confirmation requise | +| OP-11 | Rapport formation (PDF) | Admin | M | TR | Bridge endpoint /reports/formation/:id | Export PDF | +| OP-12 | Rapport formateur (PDF) | Admin | M | TR | Bridge endpoint /reports/formateur/:id | Export PDF | +| OP-13 | Inviter client par lien | Admin | M | TR | Docmost share dialog | Lien expirable | +| OP-14 | Backup quotidien | System | B | B (03:00) | Cron + Makefile target | pg_dump + tar | + +## 3. Vue par acteur + +### Admin + +```mermaid +flowchart LR + A([Admin]) --> OP01[Creer formation] + A --> OP02[Decomposer] + A --> OP03[Attribuer] + A --> OP05[Annuler attribution] + A --> OP06b[Cloturer module manuel] + A --> OP10[Archiver] + A --> OP11[Rapport formation] + A --> OP12[Rapport formateur] + A --> OP13[Inviter client] +``` + +Charge typique : ~1-2h / semaine de saisie + lectures rapports a la demande. + +### Formateur + +```mermaid +flowchart LR + F([Formateur]) --> OP04[Saisir heures realisees] + F --> OP14b[Consulter ses attributions] + F --> OP15[Editer pages wiki autorisees] +``` + +Charge typique : 5-10 min en fin de chaque session de formation pour saisir les heures. + +### System (auto) + +```mermaid +flowchart LR + S([System]) --> OP06a[Cloturer module auto] + S --> OP07[Cloturer formation auto] + S --> OP08[Recalculer rollups] + S --> OP09[Notifier depassement] + S --> OP14[Backup quotidien] +``` + +Charge : transparent. Cron daily backup, webhooks rollups TR. + +## 4. Repartition Outils + +```mermaid +flowchart TB + subgraph "Baserow" + OP01[OP-01 Creer formation] + OP02[OP-02 Decomposer] + OP03[OP-03 Attribuer] + OP05[OP-05 Annuler] + OP08[OP-08 Recalc rollups
natif] + end + subgraph "Bridge service" + OP04[OP-04 Saisie heures
UI mobile] + OP07[OP-07 Cloturer formation
cron] + OP09[OP-09 Notifier depassement
webhook + SMTP] + OP11[OP-11 Rapport formation
PDF] + OP12[OP-12 Rapport formateur
PDF] + end + subgraph "Docmost" + OP13[OP-13 Inviter client
share link] + OP15[OP-15 Editer wiki] + end + subgraph "Cron host" + OP14[OP-14 Backup quotidien] + end +``` + +## 5. Postes de travail / Devices + +| Acteur | Device principal | Browser | Mobile-friendly attendu ? | +|--------|------------------|---------|---------------------------| +| Admin | Desktop | Firefox / Chrome | Non prioritaire | +| Formateur | Mobile + Desktop | Chrome mobile / Firefox | **OUI** — saisie heures rapide en fin de session | +| Etudiant | Mobile + Desktop | Tous | **OUI** | +| Client | Mobile + Desktop | Tous | OUI | + +**Implication** : le bridge UI pour OP-04 (saisie heures) doit etre **mobile-first**. Baserow's default UI est desktop-oriented — donc soit on fait un formulaire Baserow public, soit on construit un mini-form dans le bridge. + +## 6. Planning des operations automatiques + +| Operation | Frequence | Heure | Owner | +|-----------|-----------|-------|-------| +| OP-08 Recalcul rollups | Sur evenement TR | — | Baserow natif | +| OP-07 Cloturer formation | Toutes les heures | xx:00 | Cron bridge | +| OP-09 Notifier depassement | Sur evenement TR | — | Bridge webhook | +| OP-14 Backup quotidien | Quotidien | 03:00 | Cron host | +| Audit log retention | Mensuel | 1er du mois 04:00 | Cron host | + +## 7. SLA / Contraintes operationnelles + +| Aspect | SLA cible | Justification | +|--------|-----------|---------------| +| Recalcul rollups | < 5s | UX : eviter latence visible apres saisie | +| Backup recovery (RPO) | 24h max | Backup quotidien | +| Backup recovery (RTO) | 4h max | Restauration manuelle assistee | +| Disponibilite stack | 99% (= 3.65j down/an acceptable) | Pas SI critique, c'est de l'interne | +| Latence saisie heures | < 2s | Frustration formateur sinon | + +## 8. Operations Agence (extension scope B) + +| Code | Operation (MCT) | Acteur | Type | Temps | Outil | +|------|-----------------|--------|------|-------|-------| +| OPA-01 | Creer client | Admin | M | TR | Baserow UI | +| OPA-02 | Creer projet | Admin | M | TR | Baserow UI | +| OPA-03 | Decomposer en taches | Admin | M | TR | Baserow UI (vue kanban) | +| OPA-04 | Attribuer tache a dev | Admin | M | TR | Baserow UI | +| OPA-05 | Saisir intervention | Developpeur | M | TR | Bridge UI mobile-friendly | +| OPA-06 | Marquer tache done | Dev / Admin | M | TR | Baserow UI ou bridge | +| OPA-07 | Cloturer projet | Admin | M | TR | Baserow UI (action sensible) | +| OPA-08 | Recalculer rollups Agence | System | A | TR | Baserow rollup natif | +| OPA-09 | Lier projet a formation pedago | Admin | M | TR | Baserow UI | +| OPA-10 | Recalculer capacite Personne unifiee | System | A | TR | Baserow + bridge si formules complexes | + +### Vue par acteur — Developpeur (nouveau) + +```mermaid +flowchart LR + D([Developpeur]) --> OPA05[Saisir intervention] + D --> OPA08b[Consulter ses taches] + D --> OPA06[Marquer tache done] + D --> OPA15[Editer docs technique wiki] +``` + +Charge typique : 5-10 min par jour de travail pour saisir les heures par tache. + +### SLA Agence + +| Aspect | SLA cible | Justification | +|--------|-----------|---------------| +| Latence saisie intervention | < 2s | Frustration dev sinon | +| Recalcul rollups capacite Personne | < 5s | UX dashboards admin | +| Backup Baserow Agence | 24h max | RPO acceptable | + +### Postes de travail Developpeur + +| Acteur | Device principal | Mobile-friendly | +|--------|------------------|-----------------| +| Developpeur | Desktop (IDE) + Mobile (saisie heures rapide) | OUI | + +## 9. Questions ouvertes pour validation + +- [ ] OP-04 saisie heures : formulaire web bridge OU formulaire public Baserow OU app mobile dediee ? +- [ ] OP-09 notification : email ? Slack ? Webhooks vers outil interne ? +- [ ] OP-14 backup : sur le meme host ou push S3 distant (Backblaze, OVH Object Storage) ? +- [ ] Audit log : duree de retention ? Format (json append-only / table dediee) ? +- [ ] Multi-langue UI : aujourd'hui FR seul, mais Baserow et Docmost supportent EN nativement diff --git a/docs/10-state-diagrams.md b/docs/10-state-diagrams.md new file mode 100644 index 0000000..da6afd1 --- /dev/null +++ b/docs/10-state-diagrams.md @@ -0,0 +1,202 @@ +# State Diagrams — Cycle de vie des entites + +> Vue dynamique des entites du domaine. Scope B (CFA + Agence). +> Methodologie : UML state machine. + +## 1. Pourquoi des state diagrams + +Les enums `*_statut` du MCD ne disent pas **quelles transitions sont autorisees**. Un cycle de vie explicite : +- Donne une regle de gestion claire (RG-XX par transition) +- Sert de spec UI (boutons disponibles selon l'etat) +- Aide la validation metier +- Sert de checklist pour les tests + +# Section CFA + +## 2. FORMATION + +```mermaid +stateDiagram-v2 + [*] --> draft : Creer formation + + draft --> actif : Activer (au moins 1 bloc + 1 module) + draft --> archive : Annuler avant lancement + + actif --> termine : Date fin atteinte ET tous modules realises + actif --> archive : Annulation justifiee + + termine --> archive : Archiver retention reglementaire + + archive --> [*] +``` + +- **RG-FORMATION-01** : `draft → actif` autorise uniquement si au moins 1 bloc avec au moins 1 module. +- **RG-FORMATION-02** : `actif → termine` automatique si `formation_date_fin <= NOW()` ET tous les modules en `realise` ou `annule`. + +## 3. MODULE + +```mermaid +stateDiagram-v2 + [*] --> a_attribuer : Creer module + + a_attribuer --> attribue : Premiere attribution creee + a_attribuer --> annule : Module supprime + + attribue --> en_cours : Date debut formation atteinte + attribue --> a_attribuer : Toutes attributions annulees + attribue --> annule + + en_cours --> realise : Toutes heures realisees OU date fin + en_cours --> annule + + realise --> [*] + annule --> [*] +``` + +## 4. ATTRIBUTION (Module-Personne formateur) + +```mermaid +stateDiagram-v2 + [*] --> planifie : Admin attribue module + + planifie --> en_cours : Date debut OU formateur demarre + planifie --> annule + + en_cours --> realise : Formateur saisit heures et confirme + en_cours --> annule + + realise --> [*] + annule --> [*] +``` + +# Section AGENCE + +## 5. CLIENT + +```mermaid +stateDiagram-v2 + [*] --> prospect : Premier contact + + prospect --> actif : Devis signe + prospect --> archive : Pas de suite + + actif --> inactif : Pas de projet en cours + inactif --> actif : Reactivation + + actif --> archive : Cessation activite + inactif --> archive : Idem + + archive --> [*] +``` + +- **RG-CLIENT-01** : Suppression interdite tant que `projet_statut != cloture/abandonne` exists. + +## 6. PROJET + +```mermaid +stateDiagram-v2 + [*] --> devis : Demande client recue + + devis --> en_cours : Devis signe et facture + devis --> abandonne : Pas de suite client + + en_cours --> livre : Tous livrables/taches done + en_cours --> abandonne : Annulation client ou impossibilite + + livre --> cloture : Validation client + facture finale + livre --> en_cours : Reouverture pour corrections + + cloture --> [*] + abandonne --> [*] +``` + +- **RG-PROJET-01** : `devis → en_cours` necessite signature client (champ `projet_date_debut` rempli). +- **RG-PROJET-02** : `en_cours → livre` automatique quand toutes les taches sont en `done`. + +## 7. TACHE + +```mermaid +stateDiagram-v2 + [*] --> todo : Creer tache + + todo --> in_progress : Dev demarre + todo --> abandoned : Tache annulee + + in_progress --> review : Dev marque pret pour review + in_progress --> abandoned + + review --> done : Admin valide + review --> in_progress : Demande revisions + + done --> [*] + abandoned --> [*] +``` + +- **RG-TACHE-01** : `review` est optionnel (workflow facultatif). Pour les petites taches, `in_progress → done` direct possible. + +## 8. INTERVENTION (Tache-Personne developpeur) + +```mermaid +stateDiagram-v2 + [*] --> planifie : Intervention prevue (rare) + [*] --> realise : Saisie a posteriori (cas standard) + + planifie --> realise : Dev confirme execution + planifie --> annule + + realise --> annule : Annulation justifiee (rare) + + realise --> [*] + annule --> [*] +``` + +> Note : la plupart des INTERVENTIONS sont saisies a posteriori (dev fait le travail, puis loggue les heures). Le statut `planifie` est rare, utilise pour reservations bloquantes. + +# Section COMMUNS + +## 9. PERSONNE + +```mermaid +stateDiagram-v2 + [*] --> actif : Embauche / arrivee + + actif --> inactif : Depart, suspension, vacances longues + + inactif --> actif : Retour + + inactif --> [*] : Suppression\n(interdite si attributions/interventions actives) +``` + +- **RG-PERSONNE-05** : `actif → inactif` autorise meme avec attributions/interventions actives, mais bloque les **nouvelles** assignations. +- **RG-PERSONNE-06** : Suppression interdite si `personne_id` existe dans ATTRIBUTION ou INTERVENTION non-`annule` (FK ON DELETE RESTRICT). + +# Synthese + +| Entite | Etats | Evenements declencheurs | +|--------|-------|------------------------| +| FORMATION | draft, actif, termine, archive | Creation, activation, date fin, archivage | +| BLOC | (sans cycle propre) | suit la formation | +| MODULE | a_attribuer, attribue, en_cours, realise, annule | Attribution, demarrage, completion, annulation | +| ATTRIBUTION | planifie, en_cours, realise, annule | Attribution, demarrage, saisie heures, annulation | +| CLIENT | prospect, actif, inactif, archive | Devis signe, projets actifs, cessation | +| PROJET | devis, en_cours, livre, cloture, abandonne | Signature, livraison, validation client, annulation | +| TACHE | todo, in_progress, review, done, abandoned | Demarrage, soumission review, validation, annulation | +| INTERVENTION | planifie, realise, annule | Saisie a posteriori (cas standard), annulation rare | +| PERSONNE | actif, inactif | Embauche, depart, retour | + +# Implementation + +| Niveau | Implementation | +|--------|----------------| +| MLD | Champs `*_statut` typed ENUM avec valeurs limitees | +| MPD Baserow | `Single select` avec options exactement = etats | +| Logique transition | Bridge service : valide les transitions autorisees, refuse les illegales | +| UI | Boutons d'action conditionnels selon etat courant | +| Audit | Log par transition `(entity_id, from_state, to_state, by_user, at_timestamp, reason)` | +| Auto | Cron horaire pour transitions auto (date fin → termine, etc.) | + +# Questions ouvertes + +- [ ] Faut-il un audit log explicite des transitions (qui a annule quoi quand) ? Probable oui pour Qualiopi. +- [ ] Les automatisations de transition doivent-elles tourner en cron, en webhook Baserow, ou a la lecture (lazy) ? +- [ ] Faut-il un etat `en_validation` entre `realise` et "facture client" pour PROJET (workflow de validation comptable) ? diff --git a/docs/11-uml-use-cases.md b/docs/11-uml-use-cases.md new file mode 100644 index 0000000..22adaac --- /dev/null +++ b/docs/11-uml-use-cases.md @@ -0,0 +1,240 @@ +# UML — Use Cases + +> Vue UML des acteurs et de leurs cas d'usage. Scope B (CFA + Agence + Pivot Personne). +> Methodologie : Merise pour les donnees, UML pour les comportements et acteurs. + +## 1. Acteurs + +| Acteur | Role(s) Personne | Privileges principaux | +|--------|------------------|----------------------| +| **Admin** (direction, Yan, Corentin) | `admin`, `direction` | Plein controle CFA + Agence + Operations | +| **Formateur** | `formateur` (peut cumuler `developpeur`) | Vue ses attributions, saisit heures realisees, edite pages wiki | +| **Developpeur** | `developpeur` (peut cumuler `formateur`) | Vue ses interventions, saisit heures sur taches, edite docs technique | +| **Etudiant** | (hors modele Baserow) | Acces space personnel Docmost + lecture supports formation publies | +| **Client** (guest) | (hors modele Baserow) | Lecture lien partage uniquement | +| **System** | (acteur secondaire) | Calculs auto, notifications, backups | + +**Note PERSONNE pivot** : un meme individu peut cumuler plusieurs roles (ex: Yan = formateur + developpeur + admin). Sa capacite annuelle est splittee. + +## 2. Diagrammes use case (par domaine) + +Splitte en 4 sous-diagrammes pour eviter le spaghetti : CFA, Agence, Cross/Wiki, System. + +### 2.1 Use cases CFA + +```mermaid +graph LR + Admin([Admin]) + Formateur([Formateur]) + + Admin --> UC01[Creer formation] + Admin --> UC02[Decomposer blocs/modules] + Admin --> UC03[Attribuer module formateur] + Admin --> UC04[Reattribuer/annuler] + Admin --> UC05[Consulter heures formation] + Admin --> UC06[Consulter capacite formateur] + Admin --> UC07[Rapport formation] + + Formateur --> UC03 + Formateur --> UC13[Saisir heures realisees] + Formateur --> UC14[Consulter ses attributions] +``` + +### 2.2 Use cases Agence + +```mermaid +graph LR + Admin([Admin]) + Dev([Developpeur]) + + Admin --> UCA01[Creer client] + Admin --> UCA02[Creer projet] + Admin --> UCA03[Decomposer en taches] + Admin --> UCA04[Attribuer tache dev] + Admin --> UCA05[Consulter avancement projet] + Admin --> UCA06[Cloturer projet et facturer] + + Dev --> UCA07[Saisir intervention] + Dev --> UCA08[Consulter ses taches] + Dev --> UCA09[Marquer tache done] +``` + +### 2.3 Use cases Cross / Wiki + +```mermaid +graph LR + Admin([Admin]) + Formateur([Formateur]) + Dev([Developpeur]) + Etudiant([Etudiant]) + Client([Client guest]) + + Admin --> UCX01[Lier projet a formation pedagogique] + Admin --> UCX02[Consulter capacite totale Personne] + Admin --> UCX03[Ajuster split formation/agence] + Admin --> UCW01[Gerer wiki + droits] + Admin --> UCW02[Inviter client par lien partage] + Admin --> UCW03[Creer space etudiant] + + Formateur --> UCW04[Editer pages wiki] + Dev --> UCW04 + + Etudiant --> UCW05[Acces space personnel] + Etudiant --> UCW06[Lire supports formation] + Etudiant --> UCW07[Editer ses notes] + + Client --> UCW08[Lire page partagee] +``` + +### 2.4 Use cases System (auto) + +```mermaid +graph LR + System([System]) + + System --> UCS01[Recalculer rollups] + System --> UCS02[Notifier depassement capacite] + System --> UCS03[Cloturer modules/projets auto] + System --> UCS04[Backup quotidien] +``` + +## 3. Use cases CFA detailles (top prioritaires) + +### UC-01 — Creer une formation + +- **Acteur** : Admin +- **Pre-conditions** : Admin authentifie +- **Scenario** : + 1. Admin ouvre vue "Formations" + 2. Clique "Nouvelle formation" + 3. Renseigne nom, filiere, heures totales, dates, statut = `draft` + 4. Sauvegarde +- **Post-condition** : Formation creee, `heures_attribuees = 0`, statut `draft` + +### UC-03 — Attribuer module a un formateur + +- **Acteur** : Admin (ou Formateur acceptant attribution proposee) +- **Pre-conditions** : Module existe (`module_statut != annule`), Personne avec role `formateur` et statut `actif` +- **Scenario** : + 1. Admin selectionne un module dans kanban "a attribuer" + 2. Choisit Personne dans la liste (filtre par `roles contient formateur` et `heures_restantes_formation >= heures_required`) + 3. Saisit heures + dates + 4. Confirme → ATTRIBUTION creee statut `planifie` + 5. System recalcule rollups (module, bloc, formation, personne) +- **RG** : RG-01 module heures, RG-02 capacite formateur (warning), RG-PERSONNE-02 role formateur requis + +### UC-13 — Saisir heures realisees (cours) + +- **Acteur** : Formateur (Personne avec role `formateur`) +- **Pre-conditions** : Formateur authentifie, attribution active (`planifie` ou `en_cours`) +- **Scenario** : + 1. Formateur ouvre app mobile-friendly "Mes attributions" + 2. Selectionne attribution du jour + 3. Saisit `attribution_heures_realisees` + 4. Confirme + +## 4. Use cases AGENCE detailles (nouveau) + +### UCA-02 — Creer un projet client + +- **Acteur** : Admin +- **Pre-conditions** : Admin authentifie. Client existe (sinon UCA-01 d'abord). +- **Scenario** : + 1. Admin ouvre vue "Projets" filtree par client + 2. Clique "Nouveau projet" + 3. Renseigne nom, type (site_web/app/api/...), description, charge estimee, dates + 4. Optionnel : lie a une FORMATION si projet pedagogique + 5. Statut = `devis` + 6. Sauvegarde +- **Post-condition** : Projet cree, `heures_realisees = 0` + +### UCA-04 — Attribuer une tache a un developpeur + +- **Acteur** : Admin (ou developpeur s'auto-attribuant si modele permissif) +- **Pre-conditions** : Tache existe statut `todo`, Personne avec role `developpeur` actif +- **Scenario** : + 1. Admin ouvre kanban projet, vue par statut + 2. Drag tache de `todo` vers une swimlane Personne + 3. (Pas d'INTERVENTION cree a l'attribution — l'INTERVENTION sera creee a chaque saisie d'heures) +- **Post-condition** : Tache associee informellement a un dev (via property "assignee" sur la tache), prete pour saisie INTERVENTION + +> Note : la tache n'a pas de FK directe vers la personne — le lien dev-tache se fait via les INTERVENTIONS. L'assignment "logique" peut etre une property optionnelle sur tache (`tache_assignee_id`) si on veut tracer une assignation avant saisie d'heures. + +### UCA-07 — Saisir une intervention + +- **Acteur** : Developpeur +- **Pre-conditions** : Developpeur authentifie, tache existe +- **Scenario** : + 1. Dev ouvre app mobile-friendly "Mes taches" + 2. Selectionne tache du jour + 3. Saisit nombre d'heures + date + notes optionnelles (commit ref, lien PR) + 4. Confirme → INTERVENTION creee statut `realise` + 5. System recalcule rollups (tache, projet, personne) +- **RG** : `intervention_heures > 0`, RG-PERSONNE-03 role developpeur requis + +### UCA-09 — Marquer une tache comme done + +- **Acteur** : Developpeur (ou Admin) +- **Scenario** : + 1. Dev marque tache comme `review` ou `done` + 2. Si admin valide review, tache passe `done` +- **Workflow** : optionnel d'avoir un statut intermediate `review` pour validation admin + +## 5. Use cases CROSS (Personne pivot) + +### UCX-02 — Consulter capacite totale d'une Personne + +- **Acteur** : Admin +- **Pre-conditions** : Personne existe +- **Scenario** : + 1. Admin ouvre fiche Personne + 2. Voit dashboard : + - Capacite totale annuelle : 1500h + - Split formation/agence : 50/50 + - Heures attribuees formation : 400h (sur 750 alloues) + - Heures attribuees agence : 600h (sur 750 alloues) + - Heures restantes total : 500h + - Liste de ses attributions formation + - Liste de ses interventions agence +- **Post-condition** : vue 360 sur la charge d'une personne + +### UCX-03 — Ajuster split formation/agence + +- **Acteur** : Admin +- **Scenario** : + 1. Admin edite fiche Personne + 2. Modifie `split_formation_pct` / `split_agence_pct` + 3. CHECK : somme = 100 + 4. Sauvegarde → recalcul `heures_restantes_formation` et `heures_restantes_agence` +- **Cas d'usage typique** : Yan a 60% formation, 40% agence en periode peda intense, puis 30% formation, 70% agence pendant les vacances scolaires. + +## 6. Diagramme de sequence — UCA-07 (saisir intervention) + +```mermaid +sequenceDiagram + actor Dev as Developpeur + participant UI as Bridge UI mobile + participant API as Bridge API + participant Baserow + participant DB as Baserow Engine + + Dev->>UI: Saisit heures sur tache T + UI->>API: POST /interventions + API->>API: Valide heures > 0, role dev OK + API->>Baserow: POST /database/rows/intervention + Baserow->>DB: INSERT intervention + DB->>DB: Recalcul rollups (tache, projet, personne) + DB-->>Baserow: ok + Baserow-->>API: 201 row + API-->>UI: 201 ok + UI-->>Dev: Toast confirmation + nouvelle capacite restante +``` + +## 7. Cas d'usage hors-scope (a trancher) + +- Workflow d'approbation des heures realisees par admin avant facturation +- Notification automatique formateur/dev quand tache attribuee +- Generation de feuilles de presence PDF +- Integration calendrier (iCal export attributions/taches) +- Gestion des conges/indispos qui reduisent la capacite (calendrier integre) +- API publique pour clients (visualiser leurs projets en mode auto-service) diff --git a/docs/12-uml-class-diagram.md b/docs/12-uml-class-diagram.md new file mode 100644 index 0000000..11e9056 --- /dev/null +++ b/docs/12-uml-class-diagram.md @@ -0,0 +1,289 @@ +# UML Class Diagram + +> Vue orientee objet du modele. Scope B (CFA + Agence + Personne pivot). +> Apporte les **methodes** que le MCD ne montre pas. Pont entre modele de donnees et code du bridge service Phase 2. + +## 1. Pourquoi un class diagram en plus du MCD + +Le MCD montre les **donnees** (entites + attributs + relations). Le class diagram montre : +- Les **methodes** sur chaque classe +- La **visibilite** (public/private/protected) +- Les **types de relations OO** (composition, agregation, association) +- Les patterns applicables + +## 2. Diagrammes par zone + +Splitte en 3 sous-vues : CFA, Agence, Personne pivot. Plus un diagramme global simplifie pour la vue d'ensemble. + +### 2.1 Vue globale (relations seules) + +```mermaid +classDiagram + Personne -- Attribution : "role formateur" + Personne -- Intervention : "role developpeur" + Formation *-- Bloc + Bloc *-- Module + Module -- Attribution + Client -- Projet + Projet *-- Tache + Tache -- Intervention + Projet -- Formation : "projet pedagogique" +``` + +### 2.2 Zone CFA — classes detaillees + +```mermaid +classDiagram + class Formation { + +int id + +string nom + +Filiere filiere + +decimal heuresTotales + -decimal heuresAttribuees$ + +Statut statut + +activer() void + +archiver() void + +ajouterBloc(Bloc) void + +heuresRestantes() decimal + +rapportPDF() Buffer + } + class Bloc { + +int id + +string nom + +decimal heuresPrevues + -decimal heuresAttribuees$ + +ajouterModule(Module) void + +heuresRestantes() decimal + } + class Module { + +int id + +string nom + +decimal heuresPrevues + -decimal heuresAttribuees$ + -decimal heuresRealisees$ + +Statut statut + +creerAttribution(Personne, decimal, Date, Date) Attribution + +annuler() void + +cloturer() void + } + class Attribution { + +int id + +decimal heuresAttribuees + +decimal heuresRealisees + +Statut statut + +demarrer() void + +saisirHeuresRealisees(decimal) void + +cloturer() void + +annuler(string) void + } + + Formation "1" *-- "1..*" Bloc : composition + Bloc "1" *-- "1..*" Module : composition + Module "1" -- "0..*" Attribution : association +``` + +### 2.3 Zone Agence — classes detaillees + +```mermaid +classDiagram + class Client { + +int id + +string nom + +string contactPrincipal + +Email contactEmail + +Statut statut + +creerProjet(string) Projet + +archiver() void + } + class Projet { + +int id + +string nom + +Type type + +decimal chargeHeures + -decimal heuresRealisees$ + +Statut statut + +ajouterTache(string, decimal) Tache + +lierFormationPedagogique(Formation) void + +livrer() void + +cloturer() void + +rapportPDF() Buffer + } + class Tache { + +int id + +string titre + +decimal chargeHeures + -decimal heuresRealisees$ + +Priorite priorite + +Statut statut + +creerIntervention(Personne, decimal, Date) Intervention + +marquerInProgress() void + +marquerReview() void + +marquerDone() void + } + class Intervention { + +int id + +decimal heures + +Date date + +Statut statut + +annuler(string) void + } + + Client "1" -- "1..*" Projet : association + Projet "1" *-- "0..*" Tache : composition + Tache "1" -- "0..*" Intervention : association +``` + +### 2.4 Zone Personne pivot — classe + roles + +```mermaid +classDiagram + class Personne { + +int id + +string nom + +string prenom + +Email email + +decimal capaciteAnnuelle + +decimal splitFormationPct + +decimal splitAgencePct + +Set~Role~ roles + +Statut statut + -decimal heuresAttribueesFormation$ + -decimal heuresAttribueesAgence$ + +heuresRestantesFormation() decimal + +heuresRestantesAgence() decimal + +heuresRestantesTotal() decimal + +ajouterRole(Role) void + +retirerRole(Role) void + +activer() void + +inactiver() void + +rapportPDF() Buffer + } + class Attribution { + +int id + +decimal heuresAttribuees + +Statut statut + } + class Intervention { + +int id + +decimal heures + +Statut statut + } + + Personne "1" -- "0..*" Attribution : "role formateur" + Personne "1" -- "0..*" Intervention : "role developpeur" +``` + +### 2.5 Lien pedagogique cross-zone + +```mermaid +classDiagram + class Projet { + +int id + +string nom + +lierFormationPedagogique(Formation) void + } + class Formation { + +int id + +string nom + } + Projet "0..*" -- "0..1" Formation : "projet pedagogique" +``` + +**Notation** : +- `+` public, `-` private, `#` protected +- `$` champ derive/calcule (rollup ou formula, pas stocke directement) +- `*--` composition (cycle de vie partage) +- `--` association simple + +## 3. Methodes detaillees — Personne + +| Methode | Signature | Description | +|---------|-----------|-------------| +| `heuresRestantesFormation()` | `decimal` | `(capacite * split_formation_pct/100) - heures_attribuees_formation` | +| `heuresRestantesAgence()` | `decimal` | `(capacite * split_agence_pct/100) - heures_attribuees_agence` | +| `heuresRestantesTotal()` | `decimal` | `capacite - heures_attribuees_formation - heures_attribuees_agence` | +| `ajouterRole(role)` | `Role → void` | Ajoute role aux roles existants. Idempotent. | +| `retirerRole(role)` | `Role → void` | Retire role. Verifie qu'aucune attribution/intervention active n'utilise ce role. | +| `activer()` | `void` | Statut → actif | +| `inactiver()` | `void` | Statut → inactif. Bloque nouvelles assignations. | + +## 4. Methodes detaillees — Module / Tache + +### Module.creerAttribution(personne, heures, dateDebut, dateFin) + +Verifications : +- `personne.roles.contains(Role.formateur)` — sinon throw +- `RG-01` : `SUM(this.attributions.heures) + heures <= this.heuresPrevues` +- Warning si `heures > personne.heuresRestantesFormation()` + +Effet : INSERT attribution + recalcul rollups en cascade. + +### Tache.creerIntervention(personne, heures, date) + +Verifications : +- `personne.roles.contains(Role.developpeur)` — sinon throw +- `heures > 0` +- `personne.statut == actif` + +Effet : INSERT intervention + recalcul rollups (tache, projet, personne). + +## 5. Patterns OO appliques + +| Pattern | Ou | Pourquoi | +|---------|-----|----------| +| **Value Object** | Email, Decimal heures, Filiere, Type, Priorite | Immutables, validation a la construction | +| **State Pattern** | Statut sur toutes les entites avec cycle de vie | Encapsule transitions valides | +| **Repository** | PersonneRepo, ProjetRepo, etc. | Abstrait l'acces Baserow API | +| **Factory** | Module.creerAttribution(), Tache.creerIntervention() | Encapsule logique creation + validations | +| **Observer** | Webhooks Baserow → bridge listeners | Evenements rollup → recalculs | +| **Strategy** | Calcul capacite Personne (split formation/agence) | Permet de varier la regle (ex: split par periode) | + +## 6. Mapping vers le code du bridge service (Phase 2) + +```typescript +// bridge/src/domain/personne.ts +import { Decimal } from 'decimal.js'; + +export type Role = 'formateur' | 'developpeur' | 'admin' | 'direction' | 'support'; + +export class Personne { + constructor( + public readonly id: number, + public nom: string, + public prenom: string, + public email: string, + public capaciteAnnuelle: Decimal, + public splitFormationPct: Decimal, + public splitAgencePct: Decimal, + public roles: Set, + public statut: 'actif' | 'inactif', + private _heuresAttribueesFormation: Decimal, + private _heuresAttribueesAgence: Decimal, + ) { + if (!this.splitFormationPct.plus(this.splitAgencePct).equals(100)) { + throw new Error('Splits doivent sommer a 100'); + } + } + + heuresRestantesFormation(): Decimal { + const alloue = this.capaciteAnnuelle.times(this.splitFormationPct).div(100); + return alloue.minus(this._heuresAttribueesFormation); + } + heuresRestantesAgence(): Decimal { + const alloue = this.capaciteAnnuelle.times(this.splitAgencePct).div(100); + return alloue.minus(this._heuresAttribueesAgence); + } + heuresRestantesTotal(): Decimal { + return this.capaciteAnnuelle.minus(this._heuresAttribueesFormation).minus(this._heuresAttribueesAgence); + } + // ... +} +``` + +## 7. Limites du class diagram + +- Ne montre pas la persistence (deja fait par MLD) +- Ne montre pas les sequences (deja fait par UML use cases sequence diagrams) +- Redondant avec MCD sur les attributs simples + +C'est intentionnel : chaque vue eclaire un angle different. Le class diagram = angle **comportemental statique cote code**. diff --git a/docs/13-uml-activity-diagrams.md b/docs/13-uml-activity-diagrams.md new file mode 100644 index 0000000..609bcbd --- /dev/null +++ b/docs/13-uml-activity-diagrams.md @@ -0,0 +1,192 @@ +# UML — Activity Diagrams + +> Vue dynamique des **workflows complets** (parcours utilisateur de bout en bout). +> Complement aux state diagrams (cycle de vie d'une entite) et au MCT (operations isolees). +> Chaque diagramme = un parcours metier qui traverse plusieurs entites et acteurs. + +## 1. AD-01 — Attribuer un module a un formateur + +```mermaid +flowchart TD + Start([Admin demande attribution]) --> A1[Selectionner module a attribuer] + A1 --> A2{Module existe
statut != annule?} + A2 -->|Non| Err1([Erreur: module invalide]) + A2 -->|Oui| A3[Lister formateurs avec capacite >= heures requises] + A3 --> A4{Au moins 1
formateur dispo?} + A4 -->|Non| Err2([Pas de formateur dispo - ajouter capacite ou reduire heures]) + A4 -->|Oui| A5[Admin choisit formateur] + A5 --> A6[Saisir heures + dates] + A6 --> A7{Heures attribution +
existantes <= heures module?} + A7 -->|Non| Err3([RG-01 KO: depassement heures module]) + A7 -->|Oui| A8{Heures + capacite
existante > capacite annuelle?} + A8 -->|Oui| Warn[Warning: depassement capacite formateur] + A8 -->|Non| A9 + Warn --> A9[Confirmer attribution] + A9 --> A10[INSERT attribution statut=planifie] + A10 --> A11[Recalculer rollups en cascade] + A11 --> A12[Notifier formateur par email] + A12 --> End([Attribution confirmee]) +``` + +## 2. AD-02 — Saisir heures realisees (parcours formateur) + +```mermaid +flowchart TD + Start([Formateur fin de cours]) --> B1[Ouvre app sur mobile] + B1 --> B2{Authentifie?} + B2 -->|Non| B3[Login SSO ou email/password] + B3 --> B4 + B2 -->|Oui| B4[Affiche liste de mes attributions actives] + B4 --> B5[Selectionne l'attribution du jour] + B5 --> B6[Saisit heures realisees] + B6 --> B7{heures_realisees
<= heures_attribuees + tolerance?} + B7 -->|Non| B8[Saisit raison du depassement] + B8 --> B9 + B7 -->|Oui| B9[Confirme] + B9 --> B10[UPDATE attribution.heures_realisees] + B10 --> B11{heures_realisees
== heures_attribuees?} + B11 -->|Oui| B12[attribution.statut = realise] + B11 -->|Non| B13[attribution.statut reste en_cours] + B12 --> B14[Recalculer rollups] + B13 --> B14 + B14 --> End([Saisie confirmee]) +``` + +## 3. AD-03 — Cycle complet d'une formation (vue globale) + +```mermaid +flowchart TD + Start([Direction valide nouvelle formation]) --> C1[Admin cree FORMATION statut=draft] + C1 --> C2[Admin cree BLOC RNCP-1, BLOC RNCP-2, ...] + C2 --> C3[Admin cree MODULES dans chaque bloc] + C3 --> C4{Tous modules
prets a attribuer?} + C4 -->|Non| C3 + C4 -->|Oui| C5[Admin active formation: statut=actif] + C5 --> C6{Pour chaque module} + C6 --> C7[AD-01: Attribuer formateur] + C7 --> C6 + C6 -->|Tous attribues| C8[Date debut atteinte] + C8 --> C9[Modules passent en_cours auto] + C9 --> C10{Pour chaque session} + C10 --> C11[AD-02: Formateur saisit heures] + C11 --> C10 + C10 -->|Toutes sessions terminees| C12[Modules passent realise auto] + C12 --> C13{Tous modules realises
+ date_fin atteinte?} + C13 -->|Non| C10 + C13 -->|Oui| C14[Formation statut=termine] + C14 --> C15[Generer rapport final + archivage] + C15 --> End([Formation terminee]) +``` + +## 4. AD-04 — Parcours projet client (Agence) + +> Note : couvre la branche **Agence dev** d'Acadenice. Necessite l'extension du modele decrite dans `02-scope-etendu-cfa-agence.md`. + +```mermaid +flowchart TD + Start([Client signe devis]) --> D1[Admin cree CLIENT si nouveau] + D1 --> D2[Admin cree PROJET lie au client] + D2 --> D3[Decouper en LIVRABLES ou TACHES] + D3 --> D4[Estimer charge en heures] + D4 --> D5[Identifier devs disponibles
capacite agence restante] + D5 --> D6[INTERVENTION: lier dev a tache, heures] + D6 --> D7{Dev = formateur aussi?} + D7 -->|Oui| D8[Verifier capacite totale
= capacite_agence + capacite_formation] + D7 -->|Non| D9 + D8 --> D9[Confirmer intervention] + D9 --> D10{Pour chaque session de travail} + D10 --> D11[Dev saisit heures realisees] + D11 --> D10 + D10 -->|Tache terminee| D12[Tache statut=livre] + D12 --> D13{Tous livrables
livres?} + D13 -->|Non| D10 + D13 -->|Oui| D14[PROJET statut=cloture] + D14 --> D15[Facturation client] + D15 --> End([Projet cloture]) +``` + +## 5. AD-05 — Allocation capacite Formateur-Developpeur (cas mixte) + +> Cas specifique Acadenice : un formateur peut aussi etre dev sur projet client. Sa capacite totale annuelle est splittee entre formation et agence. + +```mermaid +flowchart TD + Start([Personne avec role formateur + developpeur]) --> E1[Capacite totale annuelle = X heures] + E1 --> E2[Repartition par defaut:
50 pct formation, 50 pct agence
configurable par personne] + E2 --> E3{Allocation cours
ou projet?} + + E3 -->|Cours formation| E4[heures_cours_attribuees
+= nouvelles heures] + E4 --> E5{capacite_formation_restante >= 0?} + E5 -->|Non| Err1[Bloquer ou warning] + E5 -->|Oui| E6[OK] + + E3 -->|Projet client| E7[heures_projet_attribuees
+= nouvelles heures] + E7 --> E8{capacite_agence_restante >= 0?} + E8 -->|Non| Err2[Bloquer ou warning] + E8 -->|Oui| E9[OK] + + E6 --> Final[Recalculer:
capacite_totale_restante = X - cours_attribuees - projet_attribuees] + E9 --> Final + Final --> End([Affichage tableau de bord
Personne]) +``` + +## 6. AD-06 — Inscription etudiant a une formation + +```mermaid +flowchart TD + Start([Prospect candidate]) --> F1[Admin cree ETUDIANT prospect] + F1 --> F2[Phase selection: tests, entretiens] + F2 --> F3{Selectionne?} + F3 -->|Non| F4[Statut = refuse, lettre] + F4 --> EndKo([Fin]) + F3 -->|Oui| F5[Etudiant statut = admis] + F5 --> F6[INSCRIPTION: lier ETUDIANT a FORMATION] + F6 --> F7[Creer space personnel Docmost] + F7 --> F8[Envoyer onboarding info] + F8 --> F9{Date debut formation?} + F9 -->|Non encore| F8 + F9 -->|Oui| F10[Etudiant.statut = en_cours] + F10 --> F11[Formation classique] + F11 --> F12{Reussi?} + F12 -->|Oui| F13[Etudiant.statut = diplome] + F12 -->|Non| F14[Etudiant.statut = abandonne ou redouble] + F13 --> EndOk([Diplome remis]) + F14 --> EndKo +``` + +## 7. AD-07 — Validation et publication d'une page wiki + +```mermaid +flowchart TD + Start([Auteur ouvre page Docmost]) --> G1[Edite contenu] + G1 --> G2[Sauvegarde brouillon auto] + G2 --> G3{Pret a publier?} + G3 -->|Non| G1 + G3 -->|Oui| G4[Demande review optionnelle] + G4 --> G5{Reviewer assigne?} + G5 -->|Non| G7 + G5 -->|Oui| G6[Reviewer commente] + G6 --> G7[Auteur applique changements] + G7 --> G8[Publie page] + G8 --> G9[Indexation search] + G9 --> G10{Page partagee externe?} + G10 -->|Oui| G11[Genere share link] + G11 --> G12[Envoie lien aux destinataires] + G10 -->|Non| End + G12 --> End([Page publiee]) +``` + +## 8. Liste des activity diagrams a faire (post-validation scope) + +| Code | Titre | Statut | +|------|-------|--------| +| AD-01 | Attribuer module a formateur | Fait | +| AD-02 | Saisir heures realisees | Fait | +| AD-03 | Cycle complet d'une formation | Fait | +| AD-04 | Parcours projet client (Agence) | Brouillon, depend de scope etendu | +| AD-05 | Allocation Formateur-Dev (capacite mixte) | Brouillon, depend de scope etendu | +| AD-06 | Inscription etudiant | Fait | +| AD-07 | Validation page wiki | Fait | +| AD-08 | Onboarding nouveau salarie / formateur | A faire | +| AD-09 | Maintenance site web client (recurring) | A faire si scope etendu | +| AD-10 | Backup + restauration disaster recovery | A faire (ops) | diff --git a/docs/14-repo-structure-gitops.md b/docs/14-repo-structure-gitops.md new file mode 100644 index 0000000..98d8437 --- /dev/null +++ b/docs/14-repo-structure-gitops.md @@ -0,0 +1,516 @@ +# Repo Structure & GitOps + +> Specification du monorepo GitHub : arborescence, branching, CI/CD, DevOps, SecOps, quality gates. +> A valider avant creation du repo public. +> Audience : Corentin (DevOps owner), Yan, freelance ponctuel. + +## 1. Principes + +| Principe | Pourquoi | +|----------|----------| +| **Monorepo** | Tout versionne ensemble (infra, donnees, code custom, docs) — un tag = etat coherent | +| **Trunk-based development** | Branches courtes (max 2-3j), merge direct sur `main` apres review + CI | +| **Infra as code** | Tout ce qui touche a l'infra (compose, traefik labels, makefile, ci) versionne | +| **Secrets exclus du versioning** | Variables d'env via `.env` ignore + GitHub Secrets pour CI/CD | +| **Quality gates obligatoires** | Pas de merge sans CI vert (lint + tests + security scan) | +| **Reproductibilite** | `git clone + .env + make up` = stack identique, partout | + +## 2. Arborescence cible du repo + +``` +formation-hub/ +├── .github/ +│ ├── workflows/ +│ │ ├── ci.yml # tests + lint + security scan a chaque push/PR +│ │ ├── deploy-staging.yml # deploy automatique sur push main +│ │ ├── deploy-prod.yml # deploy sur tag v* +│ │ └── nightly-backup-test.yml # test mensuel restauration backup +│ ├── ISSUE_TEMPLATE/ +│ │ ├── bug_report.md +│ │ ├── feature_request.md +│ │ └── security.md +│ ├── PULL_REQUEST_TEMPLATE.md +│ ├── CODEOWNERS # qui review quoi +│ └── dependabot.yml # auto bumps deps +│ +├── docs/ # documentation projet (push aussi sur Outline) +│ ├── 01-discovery-recap.md +│ ├── 02-scope-etendu-cfa-agence.md +│ ├── 03-decision-record.md # ADR +│ ├── 04-cahier-des-charges-techniques.md +│ ├── 05-data-dictionary.md +│ ├── 06-merise-mcd.md +│ ├── 07-merise-mld.md +│ ├── 08-merise-mct.md +│ ├── 09-merise-mot.md +│ ├── 10-state-diagrams.md +│ ├── 11-uml-use-cases.md +│ ├── 12-uml-class-diagram.md +│ ├── 13-uml-activity-diagrams.md +│ ├── 14-repo-structure-gitops.md # CE DOC +│ ├── 15-baserow-mpd.md +│ ├── 16-plan-tests.md +│ ├── 17-plan-deployment.md +│ └── 18-plan-operations.md +│ +├── compose.yml # stack locale dev +├── compose.staging.yml # override staging (labels Traefik staging) +├── compose.prod.yml # override prod (labels prod, replicas, healthcheck strict) +│ +├── docmost/ +│ ├── README.md # specifique a la branche Docmost +│ ├── patches/ # diffs upstream (Phase 2+ si fork) +│ └── Dockerfile.fork # Phase 2+ si custom build +│ +├── baserow/ +│ ├── README.md +│ ├── schemas/ # JSON exports des tables (versionnes) +│ │ ├── personne.json +│ │ ├── formation.json +│ │ ├── ... +│ ├── seed/ +│ │ ├── seed.py # script idempotent setup initial +│ │ └── fixtures/ # data de test +│ └── migrations/ # migrations versionnees (apres setup initial) +│ +├── bridge/ # service Node TS Phase 2+ +│ ├── README.md +│ ├── package.json +│ ├── tsconfig.json +│ ├── biome.json # lint + format Biome (rapide, no config) +│ ├── Dockerfile +│ ├── src/ +│ │ ├── index.ts +│ │ ├── domain/ # domain models (Personne, Module, Tache, ...) +│ │ ├── adapters/ # baserow-client, docmost-client, redis-cache +│ │ ├── routes/ # API endpoints +│ │ ├── webhooks/ # baserow webhook handlers +│ │ └── lib/ # utils +│ ├── tests/ +│ │ ├── unit/ +│ │ ├── integration/ +│ │ └── e2e/ +│ └── .env.example +│ +├── traefik/ # config Traefik si versionnee (sinon reseau Docker existant) +│ ├── README.md +│ └── dynamic-config.yml +│ +├── scripts/ +│ ├── backup.sh # appele par cron host +│ ├── restore.sh # restauration assistee +│ ├── healthcheck.sh # check rapide endpoints +│ └── seed-baserow.sh # wrapper du baserow/seed/seed.py +│ +├── .gitignore +├── .editorconfig +├── .env.example +├── Makefile # commandes ops standardisees +├── LICENSE # AGPL-3.0 (compatible avec Docmost AGPL) +├── SECURITY.md # politique de divulgation +├── CONTRIBUTING.md # convention commits, PR, branches +├── CHANGELOG.md # tenu a jour par release +└── README.md # quickstart + lien vers docs/ +``` + +## 3. Branching strategy + +**Trunk-based development simplifie** : + +``` +main (protege) + ├── feat/saisie-heures-ui (max 2-3j vie) + ├── fix/baserow-rollup-cache (max 1j vie) + ├── chore/bump-deps (auto via dependabot) + └── ... +``` + +| Aspect | Regle | +|--------|-------| +| Branche par defaut | `main` | +| Protection `main` | Required reviews 1+, CI must pass, no force push | +| Convention nom branche | `/` ou type = `feat \| fix \| chore \| docs \| refactor \| test` | +| Duree de vie max d'une branche | 3 jours (rebase ou drop si plus vieux) | +| Squash merge | Oui, un commit propre par PR | +| Tags | `v..` (semver) declenche deploy prod | + +## 4. Convention de commits + +Format : `(): ` + +| Type | Usage | +|------|-------| +| `feat` | Nouvelle fonctionnalite | +| `fix` | Bug fix | +| `docs` | Documentation seulement | +| `refactor` | Refactor sans changement comportement | +| `test` | Ajout/modif tests | +| `chore` | Maintenance, deps, tooling | +| `ops` | Infra, CI/CD, ops | +| `sec` | Security fix ou hardening | + +Exemples : +- `feat(bridge): add formateur mention tiptap node` +- `fix(baserow): correct rollup cache invalidation on annulation` +- `ops(ci): add SAST scan with semgrep` +- `sec(deps): bump postgres to 16.4 for CVE-2026-XXXX` + +**Pas d'emoji** dans les commits, pas de signature Claude (regle Acadenice). + +## 5. CI/CD GitHub Actions + +### 5.1 Workflow `ci.yml` (a chaque push + PR) + +```yaml +name: CI +on: [push, pull_request] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npx biome check bridge/ + + type-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: 22 } + - run: cd bridge && npm ci && npm run typecheck + + test: + runs-on: ubuntu-latest + services: + postgres: { image: postgres:16-alpine, env: { POSTGRES_PASSWORD: test }, ports: ["5432:5432"] } + redis: { image: redis:7-alpine, ports: ["6379:6379"] } + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: 22 } + - run: cd bridge && npm ci && npm run test + + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Secret scanning + uses: trufflesecurity/trufflehog@main + - name: SAST + uses: returntocorp/semgrep-action@v1 + - name: Dependency check + run: cd bridge && npm audit --audit-level=high + - name: License check + run: cd bridge && npx license-checker --failOn 'GPL-3.0;AGPL-3.0' --excludePackages 'bridge' + + docker-build: + runs-on: ubuntu-latest + needs: [lint, type-check, test, security] + steps: + - uses: actions/checkout@v4 + - run: docker compose build + - run: docker compose up -d + - run: ./scripts/healthcheck.sh + - run: docker compose down -v +``` + +### 5.2 Workflow `deploy-staging.yml` (sur push main) + +```yaml +name: Deploy Staging +on: + push: + branches: [main] +jobs: + deploy: + runs-on: ubuntu-latest + environment: staging + steps: + - uses: actions/checkout@v4 + - name: Build & push images + run: | + docker build -t registry.acadenice.fr/formation-hub/bridge:${{ github.sha }} bridge/ + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.acadenice.fr -u "${{ secrets.REGISTRY_USER }}" --password-stdin + docker push registry.acadenice.fr/formation-hub/bridge:${{ github.sha }} + - name: Deploy via SSH + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.STAGING_HOST }} + username: ${{ secrets.STAGING_USER }} + key: ${{ secrets.STAGING_SSH_KEY }} + script: | + cd /opt/formation-hub + git pull + docker compose -f compose.yml -f compose.staging.yml pull + docker compose -f compose.yml -f compose.staging.yml up -d + ./scripts/healthcheck.sh +``` + +### 5.3 Workflow `deploy-prod.yml` (sur tag v*) + +Identique a staging mais cible prod, requiert approbation manuelle (`environment: production` avec required reviewers dans GitHub UI). + +### 5.4 Workflow `nightly-backup-test.yml` (mensuel) + +Restauration automatique d'un backup recent sur env isole + verification integrite. Alerte si fail. + +## 6. DevOps — environnements + +| Env | Host | Domain | Branche source | Auto deploy | +|-----|------|--------|----------------|-------------| +| **local** | machine de dev | `localhost:3000/8080/4000` | n'importe | manuel via `make up` | +| **staging** | VPS Hetzner staging | `wiki.staging.acadenice.fr` etc. | `main` | oui sur push | +| **prod** | VPS Hetzner prod | `wiki.acadenice.fr` etc. | tags `v*` | oui mais avec approval review | + +### Differences env + +| Aspect | local | staging | prod | +|--------|-------|---------|------| +| Donnees | seed fixtures | data realiste anonymisee | data reelle | +| Backups | aucun | quotidien local | quotidien local + S3 distant | +| Healthchecks | optional | active | strict + alerting | +| Replicas | 1 | 1 | 1 (peut passer a 2 si charge) | +| Logs | stdout | persisted 7j | persisted 90j + push central | +| Monitoring | aucun | uptime basique | uptime + perf + alerting | + +## 7. Secret management + +**Exclus de git, sans exception.** + +| Type secret | Stockage | +|-------------|----------| +| Local dev | `.env` (gitignored) | +| GitHub Actions | GitHub Secrets (env-scoped : staging, production) | +| Staging/Prod runtime | `.env.staging` / `.env.prod` sur le serveur, ownership root, perms 600 | +| API tokens longue duree (Outline, etc.) | Vault / pass / 1Password en interne — exclus du disque non-encrypted | +| Backup encryption key | Hors git, hors GitHub, conserve dans coffre-fort hors-bande | + +**Rotation** : tokens API rotates annuellement, secrets sensibles (DB, JWT) rotates trimestriellement. + +## 8. SecOps + +### 8.1 Quality gates avant merge + +Tous obligatoires (PR bloquee si rouge) : +- [ ] CI lint vert (Biome) +- [ ] Type-check vert (tsc) +- [ ] Tests unit + integration verts +- [ ] Coverage >= 70% sur fichiers modifies (decision a affiner) +- [ ] Secret scanning (TruffleHog) zero hit +- [ ] SAST (Semgrep) zero finding `error` severity +- [ ] Dependency check (npm audit) zero CVE `high` ou `critical` +- [ ] License check (license-checker) pas de GPL/AGPL non-compatibles +- [ ] Docker build OK + healthcheck stack passe +- [ ] Review humaine 1+ approval + +### 8.2 Outils SecOps + +| Outil | Role | Frequence | +|-------|------|-----------| +| **TruffleHog** | Secret scanning dans le code | A chaque push | +| **Semgrep** | SAST (Static Application Security Testing) | A chaque push | +| **npm audit** | CVE deps Node | A chaque push | +| **Dependabot** | Auto-bump deps + security alerts | Auto, hebdomadaire | +| **Trivy** ou **Grype** | Scan vulnerabilities images Docker | Avant push registry | +| **OWASP ZAP** | DAST sur staging (Phase 2+) | Hebdomadaire | +| **Falco** ou logs analysis | Runtime intrusion detection | Continu prod | + +### 8.3 Politique de divulgation (`SECURITY.md`) + +- Email contact : security@acadenice.fr +- Reponse sous 48h ouvrees +- Disclosure responsable, embargo coordonne +- CVE assignee si publique + +### 8.4 Audit log applicatif + +Operations sensibles loggees avec : +- Acteur (personne_id) +- Action (creation/modification/suppression/partage_externe) +- Cible (entity_type + entity_id) +- Timestamp +- IP source +- Justification (si fournie) + +Stockage : table dediee `audit_log` ou journal append-only fichier (a trancher MPD). +Retention : 5 ans (Qualiopi-compatible). + +## 9. PR template + +```markdown +## Description + + +## Type de changement +- [ ] feat +- [ ] fix +- [ ] docs +- [ ] refactor +- [ ] test +- [ ] chore +- [ ] ops +- [ ] sec + +## Issue liee +Closes #... + +## Tests realises +- [ ] Tests unit ajoutes/modifies +- [ ] Tests integration ajoutes/modifies +- [ ] Test manuel local + +## Checklist +- [ ] CI vert +- [ ] Pas de secret commit (verifier diff) +- [ ] Doc mise a jour si necessaire +- [ ] Migration data si schema change +- [ ] Changelog mis a jour si user-facing +``` + +## 10. Issue templates + +### Bug report +```markdown +## Description + + +## Etapes pour reproduire +1. ... + +## Comportement attendu + + +## Comportement observe + + +## Env (local/staging/prod) +- Version (commit SHA ou tag) : +- Browser/device : + +## Logs / screenshots + +``` + +### Feature request +```markdown +## Probleme metier + + +## Solution proposee + + +## Alternatives considerees + + +## Impact estime + +``` + +### Security +**Privee** par defaut. Reporting via email security@acadenice.fr. + +## 11. Release process + +``` +1. Merge PRs sur main → deploy staging auto +2. Tester sur staging (qualif metier + smoke tests) +3. Si OK : + - Update CHANGELOG.md (section "Unreleased" → version) + - Tag : git tag -a v1.2.3 -m "Release v1.2.3" + - git push origin v1.2.3 +4. GitHub Action deploy-prod se declenche +5. Approval manual review (Yan ou Corentin) +6. Deploy prod execute +7. Post-deploy : surveiller logs + metriques 30 min +8. Si issue : execute rollback (cf section 12) +``` + +Convention semver : +- MAJOR : breaking changes (migration data forcee, rupture API) +- MINOR : nouvelle feature, backward-compatible +- PATCH : bug fix, security fix + +## 12. Rollback process + +| Scenario | Action | +|----------|--------| +| Bug critique en prod (data loss / down) | Re-deploy version precedente : `git checkout v1.2.2 && deploy-prod.yml` | +| Schema migration foireuse | Restore Postgres depuis backup precedant le deploy + redeploy version stable | +| Compromission credentials | Rotate secrets immediate + audit logs + isoler env si necessaire | +| Bug minor en staging | Hotfix sur main, redeploy staging, ne pas tag prod | + +Runbook detaille dans `18-plan-operations.md` (a venir). + +## 13. CODEOWNERS + +``` +# Default +* @corentin + +# Infra & ops +/.github/workflows/ @corentin @yan +/compose*.yml @corentin +/Makefile @corentin +/scripts/ @corentin + +# Code custom +/bridge/ @corentin + +# Docs +/docs/ @corentin +``` + +## 14. Quickstart pour nouveau dev + +```bash +# 1. Clone +git clone git@github.com:acadenice/formation-hub.git +cd formation-hub + +# 2. Setup +cp .env.example .env +# editer .env avec secrets (cf SECURITY.md) + +# 3. Up +make up + +# 4. Acces +# - Docmost : http://localhost:3000 +# - Baserow : http://localhost:8080 +# - Bridge : http://localhost:4000 (Phase 2) + +# 5. Workflow contribution +git checkout -b feat/ma-feature +# code, test +git commit -m "feat(bridge): description" +git push origin feat/ma-feature +# Ouvrir PR sur GitHub, attendre CI vert + review +``` + +## 15. Etapes avant creation du repo public + +Checklist Corentin avant `git init` + `git push -u origin main` : + +- [ ] LICENSE (AGPL-3.0 par alignement avec Docmost) +- [ ] SECURITY.md +- [ ] CONTRIBUTING.md +- [ ] README.md (quickstart + liens docs) +- [ ] .gitignore complet (verifie pas de fichier sensible) +- [ ] .env.example commite, .env exclu +- [ ] Workflows CI/CD `.github/workflows/` prets (au moins ci.yml) +- [ ] PR template + issue templates +- [ ] CODEOWNERS +- [ ] dependabot.yml configure +- [ ] GitHub Secrets configures (`REGISTRY_*`, `STAGING_*`, `PROD_*`) +- [ ] Branch protection rules sur `main` actives +- [ ] Required reviewers configures sur `production` env + +Une fois tout vert : push `main`, configure Dependabot, enable security alerts. Le repo est pret. + +## 16. Questions a trancher + +- [ ] Repo **public** (open-source) ou **prive** ? Implications RGPD si etudiants/clients dans audit logs visible. +- [ ] Self-host GitLab interne plutot que GitHub (souverainete) ? Pas le cas aujourd'hui mais a noter. +- [ ] Registry images Docker : GitHub Container Registry, Harbor self-host, ou registry.acadenice.fr (a deployer) ? +- [ ] Couverture tests minimum : 70% / 80% / autre ? Strictesse vs vitesse. +- [ ] Tooling lint/format : Biome (rapide, all-in-one) vs ESLint+Prettier classique ? diff --git a/docs/15-baserow-mpd.md b/docs/15-baserow-mpd.md new file mode 100644 index 0000000..e340b4f --- /dev/null +++ b/docs/15-baserow-mpd.md @@ -0,0 +1,428 @@ +# MPD — Modele Physique de Donnees (Baserow) + +> Implementation concrete dans Baserow : 9 tables avec types exacts, formules, vues, permissions. +> Ce doc est **actionnable** : Corentin l'ouvre cote a cote avec Baserow et cree les tables une par une. +> Source : `07-merise-mld.md` (MLD relationnel) + `05-data-dictionary.md`. + +## 1. Setup initial + +### 1.1 Hierarchie Baserow + +``` +Workspace : Acadenice formation-hub +└── Database : formation-hub + ├── Tables CFA : formation, bloc, module, attribution + ├── Tables Agence : client, projet, tache, intervention + └── Table pivot : personne +``` + +### 1.2 Order de creation (dependances FK) + +Creer les tables dans cet ordre — chaque table dependant des precedentes via FK : + +1. **personne** (aucune dep) +2. **client** (aucune dep) +3. **formation** (aucune dep) +4. **bloc** (FK → formation) +5. **projet** (FK → client, optionnel FK → formation) +6. **module** (FK → bloc) +7. **tache** (FK → projet) +8. **attribution** (FK → module + personne) +9. **intervention** (FK → tache + personne) + +### 1.3 Conventions Baserow + +| Concept | Implementation Baserow | +|---------|------------------------| +| ID auto-increment (PK) | Baserow `id` natif (genere auto) | +| Nom du field | `snake_case`, prefixes par mnemonique d'entite (ex `formation_nom`, `personne_email`) | +| Field "Primary" | Baserow oblige a avoir un Primary field — choisir le nom le plus explicite (ex `formation_nom`) | +| Foreign Key | Field type `Link to table` | +| ENUM | Field type `Single select` avec options exactes | +| MULTI ENUM | Field type `Multiple select` | +| Champ calcule (formula) | Field type `Formula` | +| Champ derive d'une relation | Field type `Lookup` ou `Count` | +| Audit timestamps | Fields `Created on` + `Last modified time` natifs | +| Audit acteur | Fields `Created by` + `Last modified by` natifs | + +### 1.4 API token + +Apres creation des tables : +- Settings → API tokens → Create token +- Permissions : `read`/`create`/`update`/`delete` sur la database `formation-hub` +- Stocker dans `.env` cote bridge : `BASEROW_API_TOKEN=...` + +--- + +## 2. Table `personne` + +**Primary field** : `personne_nom` (texte affiche par defaut dans les links) + +| # | Field | Type Baserow | Parametres | Description | +|---|-------|-------------|------------|-------------| +| 1 | `personne_nom` | Text | — | Nom de famille (Primary) | +| 2 | `personne_prenom` | Text | — | Prenom | +| 3 | `personne_email` | Email | — | Email pro (unique recommande, validation cote bridge) | +| 4 | `personne_telephone` | Phone number | — | Telephone (optionnel) | +| 5 | `personne_capacite_annuelle` | Number | Decimal places: 2 | Heures totales/an | +| 6 | `personne_split_formation_pct` | Number | Decimal places: 1, default 50 | % capacite alloue formation | +| 7 | `personne_split_agence_pct` | Number | Decimal places: 1, default 50 | % capacite alloue agence | +| 8 | `personne_roles` | Multiple select | Options: `formateur`, `developpeur`, `admin`, `direction`, `support` | Roles cumules | +| 9 | `personne_statut` | Single select | Options: `actif` (default), `inactif` | Statut | +| 10 | `personne_attributions` | Link to table | Lien vers `attribution` (champ inverse auto-cree apres creation de attribution) | Toutes les attributions de cette personne | +| 11 | `personne_interventions` | Link to table | Lien vers `intervention` (apres creation) | Toutes les interventions | +| 12 | `personne_heures_attribuees_formation` | Formula | `sum(lookup('personne_attributions', 'attribution_heures_attribuees_active'))` | Rollup des attributions actives | +| 13 | `personne_heures_attribuees_agence` | Formula | `sum(lookup('personne_interventions', 'intervention_heures_active'))` | Rollup des interventions actives | +| 14 | `personne_heures_restantes_formation` | Formula | `(field('personne_capacite_annuelle') * field('personne_split_formation_pct') / 100) - field('personne_heures_attribuees_formation')` | Capacite formation restante | +| 15 | `personne_heures_restantes_agence` | Formula | `(field('personne_capacite_annuelle') * field('personne_split_agence_pct') / 100) - field('personne_heures_attribuees_agence')` | Capacite agence restante | +| 16 | `personne_heures_restantes_total` | Formula | `field('personne_capacite_annuelle') - field('personne_heures_attribuees_formation') - field('personne_heures_attribuees_agence')` | Capacite totale restante | + +**Vues recommandees** : +- `Tous` (grid, default) — tableau complet +- `Actifs` (grid, filtre `personne_statut = actif`) +- `Formateurs` (grid, filtre `personne_roles contient formateur`) +- `Developpeurs` (grid, filtre `personne_roles contient developpeur`) +- `Capacite restante` (grid, sort `personne_heures_restantes_total ascending`) + +--- + +## 3. Table `formation` + +**Primary field** : `formation_nom` + +| # | Field | Type Baserow | Parametres | Description | +|---|-------|-------------|------------|-------------| +| 1 | `formation_nom` | Text | — | Nom (Primary, unique conseille) | +| 2 | `formation_description` | Long text | rich text autorise | Description longue | +| 3 | `formation_filiere` | Single select | `dev`, `graphisme`, `marketing`, `iot`, `cybersec` | Filiere | +| 4 | `formation_heures_totales` | Number | Decimal places: 2 | Heures totales prevues | +| 5 | `formation_statut` | Single select | `draft` (default), `actif`, `termine`, `archive` | Cycle de vie | +| 6 | `formation_date_debut` | Date | format `YYYY-MM-DD` | Date debut | +| 7 | `formation_date_fin` | Date | — | Date fin | +| 8 | `formation_blocs` | Link to table | Lien vers `bloc` (apres creation bloc) | Blocs de la formation | +| 9 | `formation_projets_pedagogiques` | Link to table | Lien vers `projet` (optionnel) | Projets agence lies en pedagogique | +| 10 | `formation_heures_attribuees` | Formula | `sum(lookup('formation_blocs', 'bloc_heures_prevues'))` | Rollup heures des blocs | +| 11 | `formation_heures_restantes` | Formula | `field('formation_heures_totales') - field('formation_heures_attribuees')` | Reste a attribuer | +| 12 | `formation_created_at` | Created on | — | Audit | +| 13 | `formation_updated_at` | Last modified time | — | Audit | + +**Vues** : +- `Tous` (grid) +- `Actives` (grid, filtre `formation_statut = actif`) +- `Par filiere` (grid, group by `formation_filiere`) +- `Calendrier` (calendar view, sur `formation_date_debut`) +- `Capacite restante` (grid, sort `formation_heures_restantes ascending`) + +--- + +## 4. Table `bloc` + +**Primary field** : `bloc_nom` + +| # | Field | Type Baserow | Parametres | Description | +|---|-------|-------------|------------|-------------| +| 1 | `bloc_nom` | Text | — | Nom du bloc (Primary) | +| 2 | `bloc_description` | Long text | — | Description | +| 3 | `bloc_formation` | Link to table | Lien vers `formation` (single) | Formation parente | +| 4 | `bloc_heures_prevues` | Number | Decimal places: 2 | Heures du bloc | +| 5 | `bloc_ordre` | Number | Decimal places: 0 | Ordre dans la formation | +| 6 | `bloc_modules` | Link to table | Lien vers `module` (apres creation) | Modules du bloc | +| 7 | `bloc_heures_attribuees` | Formula | `sum(lookup('bloc_modules', 'module_heures_prevues_active'))` | Rollup heures modules actifs | +| 8 | `bloc_heures_restantes` | Formula | `field('bloc_heures_prevues') - field('bloc_heures_attribuees')` | Reste a decomposer | + +**Note** : la regle metier "un bloc a un nom unique par formation" se valide **cote bridge** ou via une vue filtree de duplication. + +**Vues** : +- `Tous` (grid) +- `Par formation` (grid, group by `bloc_formation`) + +--- + +## 5. Table `module` + +**Primary field** : `module_nom` + +| # | Field | Type Baserow | Parametres | Description | +|---|-------|-------------|------------|-------------| +| 1 | `module_nom` | Text | — | Nom du module (Primary) | +| 2 | `module_description` | Long text | — | Description | +| 3 | `module_bloc` | Link to table | Lien vers `bloc` (single) | Bloc parent | +| 4 | `module_heures_prevues` | Number | Decimal places: 2 | Heures du module | +| 5 | `module_statut` | Single select | `a_attribuer` (default), `attribue`, `en_cours`, `realise`, `annule` | Cycle de vie | +| 6 | `module_attributions` | Link to table | Lien vers `attribution` (apres creation) | Attributions du module | +| 7 | `module_heures_prevues_active` | Formula | `if(field('module_statut') = 'annule', 0, field('module_heures_prevues'))` | Pour rollup bloc (exclut annule) | +| 8 | `module_heures_attribuees` | Formula | `sum(lookup('module_attributions', 'attribution_heures_attribuees_active'))` | Rollup | +| 9 | `module_heures_realisees` | Formula | `sum(lookup('module_attributions', 'attribution_heures_realisees'))` | Rollup | + +**Vues** : +- `Tous` (grid) +- **`A attribuer`** (kanban, group by `module_statut`) — vue principale pour l'admin +- `Par bloc` (grid, group by `module_bloc`) +- `Realises` (grid, filtre `module_statut = realise`) + +--- + +## 6. Table `attribution` + +**Primary field** : `attribution_titre` (formula : nom_module + " → " + nom_personne) + +| # | Field | Type Baserow | Parametres | Description | +|---|-------|-------------|------------|-------------| +| 1 | `attribution_titre` | Formula | `concat(lookup('attribution_module', 'module_nom'), ' → ', lookup('attribution_personne', 'personne_prenom'), ' ', lookup('attribution_personne', 'personne_nom'))` | Titre auto (Primary) | +| 2 | `attribution_module` | Link to table | Lien vers `module` (single) | Module attribue | +| 3 | `attribution_personne` | Link to table | Lien vers `personne` (single) | Formateur (role formateur requis) | +| 4 | `attribution_heures_attribuees` | Number | Decimal places: 2 | Heures planifiees | +| 5 | `attribution_heures_realisees` | Number | Decimal places: 2, default 0 | Heures effectuees | +| 6 | `attribution_date_debut` | Date | — | Debut periode | +| 7 | `attribution_date_fin` | Date | — | Fin periode | +| 8 | `attribution_statut` | Single select | `planifie` (default), `en_cours`, `realise`, `annule` | Statut | +| 9 | `attribution_heures_attribuees_active` | Formula | `if(field('attribution_statut') = 'annule', 0, field('attribution_heures_attribuees'))` | Pour rollup module/personne | + +**Validation cote bridge** : +- `attribution_personne.personne_roles` doit contenir `formateur` +- `sum(attribution_heures_attribuees) for module <= module_heures_prevues` (RG-01) + +**Vues** : +- `Tous` (grid) +- **`Mes attributions`** (grid, filtre `attribution_personne = current user`) — vue formateur +- `En cours` (grid, filtre `attribution_statut = en_cours`) +- `Calendrier` (calendar view sur `attribution_date_debut` ou `attribution_date_fin`) +- `Form public` (form view) — formateur saisit ses heures realisees rapide + +--- + +## 7. Table `client` + +**Primary field** : `client_nom` + +| # | Field | Type Baserow | Parametres | Description | +|---|-------|-------------|------------|-------------| +| 1 | `client_nom` | Text | — | Nom (Primary) | +| 2 | `client_contact_principal` | Text | — | Nom + role du contact | +| 3 | `client_contact_email` | Email | — | Email contact | +| 4 | `client_contact_telephone` | Phone number | — | Telephone | +| 5 | `client_secteur` | Text | — | Secteur d'activite | +| 6 | `client_notes` | Long text | — | Notes libres | +| 7 | `client_statut` | Single select | `prospect` (default), `actif`, `inactif`, `archive` | Statut | +| 8 | `client_projets` | Link to table | Lien vers `projet` | Projets du client | +| 9 | `client_created_at` | Created on | — | Audit | + +**Vues** : +- `Tous` (grid) +- `Actifs` (grid, filtre `client_statut = actif`) +- **`Pipeline`** (kanban, group by `client_statut`) — vue commerciale + +--- + +## 8. Table `projet` + +**Primary field** : `projet_nom` + +| # | Field | Type Baserow | Parametres | Description | +|---|-------|-------------|------------|-------------| +| 1 | `projet_nom` | Text | — | Nom (Primary) | +| 2 | `projet_description` | Long text | — | Description | +| 3 | `projet_client` | Link to table | Lien vers `client` (single) | Client | +| 4 | `projet_type` | Single select | `site_web`, `app_mobile`, `api`, `infra`, `audit`, `support`, `autre` | Type | +| 5 | `projet_charge_heures` | Number | Decimal places: 2 | Charge estimee | +| 6 | `projet_date_debut` | Date | — | Date debut | +| 7 | `projet_date_fin_prevue` | Date | — | Date fin prevue | +| 8 | `projet_date_livraison` | Date | — | Date livraison effective | +| 9 | `projet_statut` | Single select | `devis` (default), `en_cours`, `livre`, `cloture`, `abandonne` | Statut | +| 10 | `projet_formation_pedagogique` | Link to table | Lien vers `formation` (single, optionnel) | Lien pedagogique | +| 11 | `projet_url` | URL | — | Site livraison | +| 12 | `projet_repository` | URL | — | Repo Git | +| 13 | `projet_taches` | Link to table | Lien vers `tache` | Taches du projet | +| 14 | `projet_heures_attribuees` | Formula | `sum(lookup('projet_taches', 'tache_charge_heures'))` | Rollup taches | +| 15 | `projet_heures_realisees` | Formula | `sum(lookup('projet_taches', 'tache_heures_realisees'))` | Rollup | +| 16 | `projet_heures_restantes` | Formula | `field('projet_charge_heures') - field('projet_heures_realisees')` | Reste a faire | + +**Vues** : +- `Tous` (grid) +- **`Pipeline`** (kanban, group by `projet_statut`) — vue principale +- `En cours` (grid, filtre `projet_statut = en_cours`) +- `Timeline` (timeline view sur date_debut → date_fin_prevue) +- `Par client` (grid, group by `projet_client`) + +--- + +## 9. Table `tache` + +**Primary field** : `tache_titre` + +| # | Field | Type Baserow | Parametres | Description | +|---|-------|-------------|------------|-------------| +| 1 | `tache_titre` | Text | — | Titre (Primary) | +| 2 | `tache_description` | Long text | — | Description | +| 3 | `tache_projet` | Link to table | Lien vers `projet` (single) | Projet parent | +| 4 | `tache_charge_heures` | Number | Decimal places: 2 | Charge estimee | +| 5 | `tache_priorite` | Single select | `faible`, `normale`, `haute`, `critique` | Priorite | +| 6 | `tache_statut` | Single select | `todo` (default), `in_progress`, `review`, `done`, `abandoned` | Statut | +| 7 | `tache_date_debut` | Date | — | Debut prevu | +| 8 | `tache_date_fin_prevue` | Date | — | Fin prevue | +| 9 | `tache_assignee` | Link to table | Lien vers `personne` (single, optionnel) | Dev assignee informellement | +| 10 | `tache_interventions` | Link to table | Lien vers `intervention` | Interventions sur cette tache | +| 11 | `tache_heures_realisees` | Formula | `sum(lookup('tache_interventions', 'intervention_heures_active'))` | Rollup | + +**Vues** : +- `Tous` (grid) +- **`Kanban`** (kanban, group by `tache_statut`) — vue principale +- `Par priorite` (grid, group by `tache_priorite`, sort par priorite) +- `Mes taches` (grid, filtre `tache_assignee = current user`) +- `Done recentes` (grid, filtre `tache_statut = done`, sort par date desc) + +--- + +## 10. Table `intervention` + +**Primary field** : `intervention_titre` (formula auto) + +| # | Field | Type Baserow | Parametres | Description | +|---|-------|-------------|------------|-------------| +| 1 | `intervention_titre` | Formula | `concat(lookup('intervention_tache', 'tache_titre'), ' - ', lookup('intervention_personne', 'personne_prenom'), ' (', totext(field('intervention_date')), ')')` | Titre auto (Primary) | +| 2 | `intervention_tache` | Link to table | Lien vers `tache` (single) | Tache concernee | +| 3 | `intervention_personne` | Link to table | Lien vers `personne` (single) | Developpeur (role developpeur requis) | +| 4 | `intervention_heures` | Number | Decimal places: 2 | Heures effectuees | +| 5 | `intervention_date` | Date | default today | Date intervention | +| 6 | `intervention_notes` | Long text | — | Notes / commit ref / lien PR | +| 7 | `intervention_statut` | Single select | `planifie`, `realise` (default), `annule` | Statut | +| 8 | `intervention_heures_active` | Formula | `if(field('intervention_statut') = 'annule', 0, field('intervention_heures'))` | Pour rollup tache/personne | + +**Validation cote bridge** : +- `intervention_personne.personne_roles` doit contenir `developpeur` +- `intervention_heures > 0` + +**Vues** : +- `Tous` (grid, sort `intervention_date desc`) +- **`Mes interventions`** (grid, filtre `intervention_personne = current user`) — vue dev +- **`Form rapide`** (form public) — saisie heures rapide mobile +- `Par projet` (grid, group by `intervention_tache.tache_projet`) +- `Cette semaine` (grid, filtre `intervention_date >= start_of_week`) + +--- + +## 11. Permissions et sharing + +### 11.1 Roles Baserow + +| Role | Membres | Capacites | +|------|---------|-----------| +| Admin workspace | Corentin, Yan, Ludo | Plein controle | +| Editor | Sophie + autres admins | Read/write toutes tables | +| Builder | Formateurs / Devs | Read/write **leur ligne** via vues filtrees + form rapide | +| Viewer | Stakeholders ponctuels | Read seul | + +**Limitation** : Baserow native permissions sont au niveau database/table, pas row-level. Pour limiter formateur/dev a leurs propres rows : +- Soit **vues filtrees partagees publiquement** (form pour saisie + grid filtree pour lecture) +- Soit **bridge service** qui filtre cote API selon `current_user_id` + +### 11.2 Forms publics pour saisie rapide + +Plus simple que de gerer les permissions row-level : +- Form public sur `attribution` — formateur saisit ses heures via lien sans compte Baserow +- Form public sur `intervention` — dev saisit son intervention idem + +Le user qui saisit n'a pas besoin de voir le reste des donnees, juste son formulaire. + +--- + +## 12. Webhooks (Phase 2 — bridge integration) + +Configurer dans Baserow → Database Settings → Webhooks : + +| Evenement | URL cible | Usage bridge | +|-----------|-----------|--------------| +| `row.created` sur `attribution` | `https://bridge.acadenice.fr/webhooks/attribution-created` | Notif formateur, recalcul cache mention | +| `row.updated` sur `attribution` | `https://bridge.acadenice.fr/webhooks/attribution-updated` | Recalcul cache, notif si statut change | +| `row.created` sur `intervention` | `https://bridge.acadenice.fr/webhooks/intervention-created` | Notif admin si depassement capacite | +| `row.updated` sur `module` (si `module_statut` change) | `https://bridge.acadenice.fr/webhooks/module-status-changed` | Trigger cloturer formation auto | + +Authentification webhook : header `X-Bridge-Token` avec un secret partage (`.env`). + +--- + +## 13. Seed data initial + +Apres creation des 9 tables, seed avec : +- **personne** : equipe Acadenice (Yan, Corentin, Ludo, Sophie + formateurs intervenants) +- **client** : 1-2 clients existants (Centralis Europe + autre) +- **formation** : les 5 filieres en cours pour 2026-2027 +- **bloc** : decoupage RNCP par filiere +- **module** : programme detaille +- **projet** : projets clients en cours +- Pas d'attribution / intervention seed — saisies par l'usage + +Script de seed : `baserow/seed/seed.py` (a coder Phase 1). + +--- + +## 14. Validation post-creation + +Checklist apres creation des 9 tables : + +- [ ] Les 9 tables existent dans la database `formation-hub` +- [ ] Toutes les FK sont liees correctement (verifier en cliquant sur un lien dans une row) +- [ ] Les rollups fonctionnent (creer une row test, verifier le calcul de `formation_heures_attribuees`) +- [ ] Les formulas s'evaluent sans erreur (regarder les rows test) +- [ ] Les Single Select / Multiple Select ont les bonnes options +- [ ] Au moins une vue par table est creee (grid `Tous` minimum) +- [ ] Les vues kanban (module, projet, tache, client) sont fonctionnelles +- [ ] Le form public pour saisie heures (attribution, intervention) marche +- [ ] L'API token est genere et fonctionne (test `curl`) + +```bash +# Test API token +curl -H "Authorization: Token $BASEROW_API_TOKEN" \ + "$BASEROW_URL/api/database/rows/table//?user_field_names=true" +``` + +--- + +## 15. Notes d'implementation + +### 15.1 Limitations Baserow connues + +- **Pas de FK `ON DELETE` configurable** : c'est `SET NULL` par defaut quand le lien est rompu. Pour forcer un comportement CASCADE/RESTRICT, le bridge service doit l'implementer (ou un workflow Baserow). +- **Pas de CHECK constraint** : validation cote bridge ou cote UI. +- **Pas d'index custom** : Baserow indexe automatiquement les Link to table et les Primary fields. +- **Formules limitees** : pas de boucles ni de subqueries complexes. Pour calculs lourds, calcul cote bridge + ecriture en batch. + +### 15.2 Alternatives si Baserow limite + +Si une formule devient trop complexe ou si on a besoin de validation forte : +- Option A : **Bridge fait le calcul** et ecrit en Baserow via API +- Option B : **Vue filtree dediee** + formula simple +- Option C : **Migration vers Postgres direct** (futur — si on perd Baserow) + +### 15.3 Migration data initiale + +Si donnees existent dans Excel/Trello/autre : +1. Exporter en CSV +2. Mapper les colonnes vers les fields Baserow +3. Importer via Baserow UI (`Import data`) +4. Verifier les liens FK manuellement (Baserow ne mappe pas auto les liens via CSV) + +--- + +## 16. Resume — checklist d'implementation Phase 1 + +``` +[ ] 1. Setup workspace + database +[ ] 2. Creer table 'personne' (sans liens encore) +[ ] 3. Creer table 'client' (sans liens encore) +[ ] 4. Creer table 'formation' (sans liens encore) +[ ] 5. Creer table 'bloc' + lien vers formation +[ ] 6. Creer table 'projet' + lien vers client (+ optionnel formation) +[ ] 7. Creer table 'module' + lien vers bloc +[ ] 8. Creer table 'tache' + lien vers projet (+ optionnel personne assignee) +[ ] 9. Creer table 'attribution' + liens vers module + personne +[ ] 10. Creer table 'intervention' + liens vers tache + personne +[ ] 11. Ajouter formulas et lookups (apres tous les liens crees) +[ ] 12. Creer vues recommandees par table +[ ] 13. Configurer permissions roles + sharing +[ ] 14. Seed data initial +[ ] 15. Generer API token + verifier +[ ] 16. Documenter exports JSON dans `baserow/schemas/` +``` + +Apres ca : la base structurelle est en place. La saisie metier peut commencer **immediat** — sans attendre Phase 2 / bridge. diff --git a/docs/16-plan-tests.md b/docs/16-plan-tests.md new file mode 100644 index 0000000..2202720 --- /dev/null +++ b/docs/16-plan-tests.md @@ -0,0 +1,259 @@ +# Plan de tests + +> Strategie de qualite : niveaux de tests, outils, coverage, criteres d'acceptance, regression. +> Audience : Corentin + freelance ponctuel (Phase 2 bridge). + +## 1. Strategie globale — Pyramide + +``` + /\ + /E2E\ peu nombreux, lents, fragiles, hauts dans la stack + /------\ + / INT \ middle — verifie les contrats Baserow/Docmost/bridge + /----------\ + / UNIT \ nombreux, rapides, isoles — bridge service uniquement + /--------------\ +``` + +- **Unit tests** : 70% du volume, sur le code custom (bridge) +- **Integration tests** : 25%, sur les vrais clients Baserow/Docmost (containers de test) +- **E2E tests** : 5%, parcours utilisateur complet sur staging +- **UX manuel + NFR** : checklist par release + +## 2. Scope du test + +Ce qui se teste : +- **Bridge service** (notre seul code custom) : 100% obligatoire +- **Configurations Baserow** (formules, vues) : tests manuels checklist +- **Configurations Docmost** (perms, share links) : tests manuels checklist +- **Workflows metier** : tests E2E des parcours principaux + +Ce qui ne se teste pas : +- Code upstream Docmost/Baserow (deja teste par eux) +- Postgres/Redis (assume fonctionnel) + +## 3. Niveaux de tests + +### 3.1 Unit tests (bridge) + +| Aspect | Spec | +|--------|------| +| Outil | Vitest | +| Cible | `bridge/src/**` — domain models, formulas, utils, validators | +| Mock | Pas de Baserow/Docmost reels, mocks via `vi.mock()` | +| Coverage minimum | 80% sur `bridge/src/domain/` et `bridge/src/lib/`, 70% global | +| Exemples a tester | `Personne.heuresRestantesTotal()`, `Module.creerAttribution()` validations RG, parsers d'entree | +| Run | `npm run test:unit` | +| CI | A chaque push + PR | + +Pattern : +```typescript +// bridge/src/domain/personne.test.ts +import { describe, it, expect } from 'vitest'; +import { Personne } from './personne'; +import { Decimal } from 'decimal.js'; + +describe('Personne.heuresRestantesTotal', () => { + it('returns capacity - allocated', () => { + const p = new Personne({ + capaciteAnnuelle: new Decimal(1500), + heuresAttribueesFormation: new Decimal(400), + heuresAttribueesAgence: new Decimal(600), + // ... + }); + expect(p.heuresRestantesTotal().toNumber()).toBe(500); + }); + + it('handles overflow (negative result)', () => { + // ... + }); +}); +``` + +### 3.2 Integration tests (bridge ↔ services) + +| Aspect | Spec | +|--------|------| +| Outil | Vitest + testcontainers (Postgres + Redis reels) | +| Cible | Adapters Baserow/Docmost, webhook handlers, cache Redis | +| Setup | Docker compose `compose.test.yml` lance Baserow et Docmost containers ephemeres | +| Coverage | Tous les endpoints du bridge | +| Run | `npm run test:integration` | +| CI | A chaque push (utilise services Postgres/Redis du runner GitHub Actions) | +| Duree max | 5 min (sinon a optimiser ou move vers nightly) | + +Exemples : +- `POST /interventions` cree bien une row dans Baserow, recalcule rollups +- `Webhook row.created` declenche cache invalidation Redis +- Auth `Authorization: Bearer ` valide ou refuse correctement + +### 3.3 E2E tests (workflows complets) + +| Aspect | Spec | +|--------|------| +| Outil | Playwright | +| Cible | Parcours metier complets sur env staging | +| Browsers | Chromium + Firefox (mobile WebKit pour saisie heures) | +| Run | `npm run test:e2e` | +| CI | Apres deploy staging reussi (pas a chaque push) | +| Duree max | 15 min | + +Parcours E2E a couvrir (top 5 priorite) : +1. **Admin cree formation → blocs → modules** (UC-01 + UC-02) +2. **Admin attribue module a un formateur** (UC-03) +3. **Formateur saisit heures realisees via mobile** (UC-13) +4. **Admin cree client → projet → taches** (UCA-01 + UCA-02 + UCA-03) +5. **Dev saisit intervention sur tache** (UCA-07) + +### 3.4 UX manuel — checklist + +Pas automatisable (parle de feel, intuitivite). Pour chaque release : + +``` +[ ] Login Docmost et Baserow OK +[ ] Saisie d'une formation prend < 30s +[ ] Saisie heures realisees mobile prend < 15s sur smartphone +[ ] Diagrammes Mermaid/Drawio/Excalidraw rendent OK dans une page Docmost +[ ] Share link client fonctionne sans compte +[ ] Recherche full-text Docmost trouve une page recente +[ ] Filtres Baserow sur les vues principales fonctionnent +[ ] Pas de regression visible sur les 3 vues kanban (modules / projets / taches) +``` + +### 3.5 NFR — tests non-fonctionnels + +| Categorie | Test | Cible | +|-----------|------|-------| +| **Performance** | Latence saisie intervention (UCA-07) | p95 < 2s | +| Performance | Recherche full-text Docmost | p95 < 500ms | +| Performance | Recalcul rollup Baserow apres saisie | < 5s | +| **Securite** | Secret scanning (TruffleHog) | Zero hit | +| Securite | SAST (Semgrep) | Zero finding `error` severity | +| Securite | Dependency CVE (npm audit) | Zero `high`/`critical` | +| Securite | Auth bypass tentative (pentest leger) | 401 sur endpoints proteges | +| **Accessibility** | Lighthouse a11y score | >= 90 sur pages publiques | +| **Charge** | 30 users simultanes (k6) | Latence p95 < 3s, error rate < 1% | +| **Backup** | Restore depuis backup recent | RTO < 4h, integrity 100% | + +## 4. Donnees de test (fixtures) + +### 4.1 Strategie + +- **Unit tests** : objects in-memory crees ad-hoc dans le test +- **Integration tests** : seed Baserow ephemere via API a chaque test (fast) +- **E2E tests** : staging environment avec data realiste anonymisee + reset post-test + +### 4.2 Fixtures versionnees + +`bridge/tests/fixtures/` +- `personnes.json` : 5 personnes types (admin pur, formateur seul, dev seul, formateur+dev, inactif) +- `formations.json` : 2 formations (1 active, 1 archivee) +- `clients.json` : 3 clients (prospect, actif, archive) +- ... + +Chargement : helper `loadFixture('personnes')` dans tests. + +## 5. Test environments + +| Env | Donnees | Usage | +|-----|---------|-------| +| `local` (dev) | Fixtures seedees a chaque `make up` | Dev quotidien | +| `test` (CI) | Containers ephemeres + fixtures | Integration tests CI | +| `staging` | Data realiste anonymisee | E2E + UX manuel | +| `prod` | Data reelle | Pas de tests destructifs | + +## 6. Quality gates + +A chaque PR, **bloquant** pour merge si rouge : + +| Check | Tool | Critere | +|-------|------|---------| +| Lint | Biome | Zero error | +| Type check | tsc | Zero error | +| Unit tests | Vitest | 100% pass | +| Integration tests | Vitest + testcontainers | 100% pass | +| Coverage unit | Vitest | >= 70% global, 80% sur domain | +| Secret scan | TruffleHog | Zero hit | +| SAST | Semgrep | Zero `error` severity | +| Dep audit | npm audit | Zero `high`/`critical` | +| Docker build | docker compose | OK | + +E2E tests **non bloquants pour merge** mais bloquants pour deploy prod (run apres deploy staging). + +## 7. Acceptance criteria par feature + +Format Gherkin pour les UC principaux : + +```gherkin +Feature: Saisir heures realisees (UC-13) + As a Formateur + I want to log my actual hours per attribution + So that the rollups update and admin sees real progress + + Scenario: Formateur saisit ses heures dans la limite + Given une attribution "Module JS / Pierre" en statut planifie avec 10h attribuees + When Pierre saisit 3h realisees + Then attribution.heures_realisees = 3h + And module.heures_realisees est recalcule + And personne.heures_attribuees_formation reste a 10h + And no warning displayed + + Scenario: Formateur saisit en depassement + Given une attribution avec 10h attribuees, deja 8h realisees + When Pierre saisit 4h supplementaires (total 12h, depasse de 2h) + Then warning "Depassement detecte, justification requise" + And attribution.heures_realisees = 12h apres confirmation justification +``` + +A faire pour chaque UC critique (UC-01, UC-03, UC-13, UCA-02, UCA-07). + +## 8. Plan de regression + +Avant chaque release vers prod : + +1. Run full test suite (unit + integration + E2E sur staging) +2. UX checklist manuel (cf section 3.4) +3. Smoke test post-deploy prod (verifier 3 endpoints critiques) +4. Verification monitoring (logs / metriques 30 min apres deploy) + +Si une regression majeure est detectee : rollback (cf doc 14 section 12). + +## 9. Test de migration data + +Lors de l'import data initial (formations existantes / formateurs / clients) : + +1. **Dry-run** : mapping CSV → Baserow rows en memoire, validation schema, rapport ecarts +2. **Test import** sur env staging avec subset (10 rows) +3. **Verification** integrite : rollups calcules correctement, FK liees +4. **Import prod** apres validation +5. **Reconciliation** : compare nb rows attendus vs imported + +## 10. Outils — recap + +| Outil | Role | Where | +|-------|------|-------| +| Vitest | Unit + integration tests | bridge/ | +| testcontainers | Postgres/Redis containers ephemeres | bridge/tests/integration | +| Playwright | E2E sur staging | bridge/tests/e2e | +| k6 | Load testing | scripts/load-test.js | +| Lighthouse | A11y + perf web | nightly via CI ou manuel | +| TruffleHog | Secret scanning | CI | +| Semgrep | SAST | CI | +| npm audit / Dependabot | Dep CVE | CI + auto | +| Biome | Lint + format | CI | + +## 11. Roadmap tests + +| Phase | Couverture | +|-------|-----------| +| Phase 1 (vanilla) | Tests manuels checklist UX, smoke tests, validation post-import data | +| Phase 2 (bridge code) | Unit + integration obligatoires sur le code bridge des le jour 1 | +| Phase 3 (maturite) | Ajouter E2E Playwright sur staging, NFR (perf + a11y), load tests | +| Phase 4 | Test de DR (restauration backup) mensuel | + +## 12. Questions ouvertes + +- [ ] Coverage minimum exacte ? Le doc 14 propose 70% — a confirmer avec Yan/Ludo +- [ ] Tests d'accessibilite obligatoires ou nice-to-have ? (RGAA conformance pour les pages publiques etudiants ?) +- [ ] Tests de charge : a partir de quelle Phase ? (proba pas avant Phase 3) +- [ ] Outils de monitoring synthetic (UptimeRobot pour healthchecks) — a definir doc 18 diff --git a/docs/17-plan-deployment.md b/docs/17-plan-deployment.md new file mode 100644 index 0000000..a9e71ab --- /dev/null +++ b/docs/17-plan-deployment.md @@ -0,0 +1,500 @@ +# Plan de deployment + +> Strategie de deploiement : provisionnement, CI/CD detaille, releases, migrations, rollback. +> Complete `14-repo-structure-gitops.md` (qui pose la structure CI/CD). + +## 1. Vue d'ensemble — 3 environnements + +```mermaid +flowchart LR + Dev[Dev local
make up
fixtures seed] --> Push[git push origin main] + Push -->|auto| Staging[Staging
wiki.staging.acadenice.fr
data anonymisee] + Staging --> Tag[git tag v1.X.Y] + Tag -->|approval review| Prod[Prod
wiki.acadenice.fr
data reelle] +``` + +| Env | Trigger deploy | Approval | Data | Target | +|-----|----------------|----------|------|--------| +| local | `make up` | — | seed fixtures | dev quotidien | +| staging | push `main` | auto | anonymisee | qualif metier + E2E | +| prod | tag `v*` | manual reviewer | reelle | utilisateurs finaux | + +## 2. Provisionnement infra + +### 2.1 Hardware cible + +| Env | Specs | Cout/mois | Provider candidat | +|-----|-------|-----------|-------------------| +| staging | 2 vCPU, 4 Go RAM, 40 Go SSD | ~7€ | Hetzner CX21 ou OVH equivalent | +| prod | 4 vCPU, 8 Go RAM, 80 Go SSD | ~15€ | Hetzner CPX31 | +| backup distant | 100 Go object storage | ~5€ | Backblaze B2 ou OVH Object Storage | + +### 2.2 OS et stack base + +- **OS** : Debian 12 (stable, support long, deja maitrise par Corentin) +- **Docker** : version 25+ via repo officiel +- **Docker Compose** : v2 (plugin standard) +- **Reverse proxy** : Traefik 3 (deja en place sur Acadenice) +- **Cron** : crond systeme pour backups nocturnes + +### 2.3 DNS et TLS + +| Sous-domaine | Pointe vers | TLS | +|--------------|-------------|-----| +| `wiki.acadenice.fr` | VPS prod | Let's Encrypt via Traefik | +| `baserow.acadenice.fr` | VPS prod | Let's Encrypt via Traefik | +| `bridge.acadenice.fr` | VPS prod | Let's Encrypt via Traefik (Phase 2+) | +| `wiki.staging.acadenice.fr` | VPS staging | Let's Encrypt | +| `baserow.staging.acadenice.fr` | VPS staging | Let's Encrypt | + +Traefik genere les certificats automatiquement via ACME (HTTP-01 challenge). + +### 2.4 Provisionnement initial (premiere fois) + +```bash +# 1. SSH sur le VPS frais +ssh root@ + +# 2. Hardening de base +adduser corentin --gecos "" +usermod -aG sudo,docker corentin +ssh-copy-id corentin@ +# Editer /etc/ssh/sshd_config : +# PermitRootLogin no +# PasswordAuthentication no +systemctl restart sshd + +# 3. Installer Docker +curl -fsSL https://get.docker.com | sh + +# 4. Cloner le repo +mkdir -p /opt/formation-hub && cd /opt/formation-hub +git clone git@github.com:acadenice/formation-hub.git . + +# 5. Configurer .env +cp .env.example .env.staging # ou .env.prod +nano .env.staging # remplir avec secrets reels + +# 6. Lancer +docker compose -f compose.yml -f compose.staging.yml up -d + +# 7. Verifier +./scripts/healthcheck.sh +``` + +Note : **a executer une seule fois** par environnement. Apres, c'est CI/CD qui prend le relais. + +## 3. CI/CD detaille + +### 3.1 Vue d'ensemble des workflows + +| Workflow | Trigger | Duree max | Bloque sur echec | +|----------|---------|-----------|------------------| +| `ci.yml` | push + PR | 10 min | Merge bloque | +| `deploy-staging.yml` | push `main` (apres CI vert) | 5 min | Pas de deploy si CI rouge | +| `deploy-prod.yml` | tag `v*` | 5 min + approval | Pas de deploy sans approval | +| `nightly-backup-test.yml` | cron 03:00 mensuel | 30 min | Alerte slack si fail | +| `e2e.yml` | apres `deploy-staging` reussi | 15 min | Pas bloquant pour staging mais bloquant pour tag prod | + +### 3.2 Workflow `ci.yml` (full) + +```yaml +name: CI +on: + push: + branches-ignore: [main] + pull_request: + branches: [main] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: 22, cache: 'npm', cache-dependency-path: 'bridge/package-lock.json' } + - run: cd bridge && npm ci + - run: cd bridge && npm run lint + + type-check: + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: 22, cache: 'npm', cache-dependency-path: 'bridge/package-lock.json' } + - run: cd bridge && npm ci + - run: cd bridge && npm run typecheck + + test-unit: + runs-on: ubuntu-latest + needs: type-check + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: 22 } + - run: cd bridge && npm ci + - run: cd bridge && npm run test:unit -- --coverage + - uses: actions/upload-artifact@v4 + with: + name: coverage-unit + path: bridge/coverage + + test-integration: + runs-on: ubuntu-latest + needs: type-check + services: + postgres: + image: postgres:16-alpine + env: { POSTGRES_PASSWORD: test, POSTGRES_DB: testdb } + ports: ['5432:5432'] + options: --health-cmd pg_isready --health-interval 5s --health-retries 10 + redis: + image: redis:7-alpine + ports: ['6379:6379'] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: 22 } + - run: cd bridge && npm ci + - run: cd bridge && npm run test:integration + env: + DATABASE_URL: postgresql://postgres:test@localhost:5432/testdb + REDIS_URL: redis://localhost:6379 + + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: { fetch-depth: 0 } + - name: Secret scanning + uses: trufflesecurity/trufflehog@main + with: + path: ./ + base: ${{ github.event.pull_request.base.sha || github.event.before }} + - name: SAST + uses: returntocorp/semgrep-action@v1 + with: { config: 'p/javascript p/typescript p/security-audit' } + - name: Dep audit + run: cd bridge && npm audit --audit-level=high + - name: License check + run: cd bridge && npx license-checker --failOn 'GPL-3.0;AGPL-3.0' --excludePackages 'bridge' + + docker-build: + runs-on: ubuntu-latest + needs: [test-unit, test-integration, security] + steps: + - uses: actions/checkout@v4 + - run: docker compose build + - run: docker compose up -d + - run: ./scripts/healthcheck.sh + - run: docker compose down -v +``` + +### 3.3 Workflow `deploy-staging.yml` + +```yaml +name: Deploy Staging +on: + push: + branches: [main] + workflow_run: + workflows: ['CI'] + types: [completed] + branches: [main] +jobs: + deploy: + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'push' }} + runs-on: ubuntu-latest + environment: staging + steps: + - uses: actions/checkout@v4 + - name: Build & push image + run: | + docker build -t registry.acadenice.fr/formation-hub/bridge:${{ github.sha }} bridge/ + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.acadenice.fr -u "${{ secrets.REGISTRY_USER }}" --password-stdin + docker push registry.acadenice.fr/formation-hub/bridge:${{ github.sha }} + - name: Deploy via SSH + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.STAGING_HOST }} + username: corentin + key: ${{ secrets.STAGING_SSH_KEY }} + script: | + cd /opt/formation-hub + git fetch && git checkout ${{ github.sha }} + export BRIDGE_IMAGE=registry.acadenice.fr/formation-hub/bridge:${{ github.sha }} + docker compose -f compose.yml -f compose.staging.yml pull + docker compose -f compose.yml -f compose.staging.yml up -d + ./scripts/healthcheck.sh + - name: Notify Slack on failure + if: failure() + uses: slackapi/slack-github-action@v1 + with: + payload: '{"text":"Deploy staging FAILED — sha ${{ github.sha }}"}' + env: { SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} } +``` + +### 3.4 Workflow `deploy-prod.yml` + +Identique mais cible prod, avec `environment: production` qui active les **required reviewers** (Yan ou Corentin doivent approuver dans GitHub UI). + +```yaml +name: Deploy Production +on: + push: + tags: ['v*'] +jobs: + deploy: + runs-on: ubuntu-latest + environment: production # required reviewers: Yan, Corentin + steps: + - uses: actions/checkout@v4 + with: { ref: ${{ github.ref_name }} } + - name: Tag image as prod + run: | + docker pull registry.acadenice.fr/formation-hub/bridge:${{ github.sha }} + docker tag registry.acadenice.fr/formation-hub/bridge:${{ github.sha }} registry.acadenice.fr/formation-hub/bridge:${{ github.ref_name }} + docker push registry.acadenice.fr/formation-hub/bridge:${{ github.ref_name }} + - name: Deploy via SSH + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.PROD_HOST }} + username: corentin + key: ${{ secrets.PROD_SSH_KEY }} + script: | + cd /opt/formation-hub + git fetch --tags && git checkout ${{ github.ref_name }} + export BRIDGE_IMAGE=registry.acadenice.fr/formation-hub/bridge:${{ github.ref_name }} + docker compose -f compose.yml -f compose.prod.yml pull + docker compose -f compose.yml -f compose.prod.yml up -d + ./scripts/healthcheck.sh + - name: Update CHANGELOG + run: # commit et push CHANGELOG dans une PR auto si pas deja fait + - name: Notify Slack + uses: slackapi/slack-github-action@v1 + with: + payload: '{"text":"PROD deployed: ${{ github.ref_name }}"}' +``` + +## 4. Strategie de release + +### 4.1 Convention semver + +| Type | Quand | Exemple | +|------|-------|---------| +| MAJOR | Breaking change | v1.x.x → v2.0.0 (changement schema Baserow incompatible) | +| MINOR | Nouvelle feature backward-compatible | v1.2.x → v1.3.0 (ajout endpoint bridge) | +| PATCH | Bug fix / security fix | v1.2.3 → v1.2.4 | + +### 4.2 Process de release + +``` +1. PRs mergees sur main → deploy staging auto +2. QA staging (UX checklist + E2E run) +3. Si OK : + - Update CHANGELOG.md (deplacer "Unreleased" → version) + - git tag -a v1.2.3 -m "Release v1.2.3 — features:..." + - git push origin v1.2.3 +4. GitHub Action deploy-prod se declenche +5. Approval review (Yan ou Corentin) +6. Deploy execute +7. Post-deploy : monitoring 30 min +8. Si issue : rollback (cf section 6) +``` + +### 4.3 Format CHANGELOG + +```markdown +## [Unreleased] + +### Added +- Feature X + +### Changed +- Y + +### Fixed +- Z + +## [1.2.3] - 2026-06-15 + +### Added +- Endpoint /personnes/:id/timeline + +### Fixed +- Recalcul rollup formation depuis attribution annulee +``` + +Convention : [Keep a Changelog](https://keepachangelog.com). + +## 5. Migration database + +### 5.1 Strategie + +- **Baserow** : modifications de schema via UI Baserow OU API. Versionner les schemas dans `baserow/schemas/*.json` (export periodique). +- **Bridge service** : pas de DB propre en Phase 2 (stateless). Si plus tard on ajoute Postgres dedie : utiliser Drizzle migrations versionnees dans `bridge/migrations/`. + +### 5.2 Pre-migration checklist + +``` +[ ] Backup Baserow + Postgres docmost FRESH avant migration +[ ] Test migration sur staging avec data realiste anonymisee +[ ] Verification integrite post-migration sur staging +[ ] Plan rollback documente (revert schema OU restore backup) +[ ] Annonce equipe : window de maintenance prevue +[ ] Migration prod sur creneau low-traffic (early morning ou weekend) +``` + +### 5.3 Pendant la migration + +```bash +# 1. Stop services (eviter ecritures concurrentes) +docker compose stop docmost baserow + +# 2. Backup explicite +make backup + +# 3. Run migration script (manuel ou via bridge command) +./scripts/migrate.sh v1.2.3 + +# 4. Verification integrite +./scripts/healthcheck.sh +./scripts/verify-rollups.sh + +# 5. Restart services +docker compose -f compose.yml -f compose.prod.yml up -d + +# 6. Smoke test post-migration +curl -fsS https://wiki.acadenice.fr/api/health +``` + +### 5.4 Communication metier + +Avant migration affectant prod : +- Email aux admins 48h avant +- Banner Docmost "maintenance prevue le X de Yh a Zh" +- Slack #internal au debut et fin + +## 6. Rollback strategy detaillee + +| Scenario | Action | Duree estimee | +|----------|--------|---------------| +| **Bug critique post-deploy prod** (regression majeure) | Re-deploy version precedente : `git tag v1.2.3-rollback v1.2.2 && git push --tags` → trigger deploy-prod sur la version stable | 5-10 min | +| **Migration schema casse rollups** | 1) `docker compose stop` 2) Restore Postgres docmost depuis backup 3) Restore Baserow data 4) Redeploy version stable | 30-60 min | +| **Compromission credentials** | 1) Revoke tokens API 2) Rotate secrets `.env.prod` 3) Redeploy 4) Audit logs 5) Communiquer si data leak | 1-4h | +| **VPS down** (provider issue) | Failover manuel vers VPS backup OU attendre provider | depend incident | +| **Bug minor en staging** | Hotfix sur main + redeploy staging. Pas de tag prod. | 10-20 min | + +### 6.1 Pre-prod rollback test + +Mensuel, sur staging : +1. Deploy une version +2. Simuler bug (fail healthcheck volontaire) +3. Re-deploy version precedente +4. Verifier que tout fonctionne +5. Logger le test dans le journal ops + +## 7. Configuration env-specific + +### 7.1 Variables par env + +| Variable | local | staging | prod | +|----------|-------|---------|------| +| `DOCMOST_URL` | http://localhost:3000 | https://wiki.staging.acadenice.fr | https://wiki.acadenice.fr | +| `BASEROW_URL` | http://localhost:8080 | https://baserow.staging.acadenice.fr | https://baserow.acadenice.fr | +| `BRIDGE_URL` (Phase 2) | http://localhost:4000 | https://bridge.staging.acadenice.fr | https://bridge.acadenice.fr | +| `LOG_LEVEL` | debug | info | warn | +| `BACKUP_S3_BUCKET` | (none) | (none) | s3://acadenice-formation-hub-backup | +| `SENTRY_DSN` (Phase 3+) | (none) | https://...sentry.io/staging | https://...sentry.io/prod | + +### 7.2 Secret management workflow + +``` +1. Generer un nouveau secret (random 32+ chars) +2. Stocker dans pass / 1Password / Vault interne +3. Set en GitHub Secret (env-scoped) +4. Set en .env.staging / .env.prod sur le serveur (perms 600, owner root) +5. Test deploy +6. Rotater l'ancien secret (revoque cote service) +``` + +### 7.3 Frequence rotation + +| Type secret | Frequence rotation | +|-------------|-------------------| +| API tokens externes (Outline, etc.) | Annuelle | +| DB passwords | Trimestrielle | +| JWT signing keys | Trimestrielle | +| SSH keys deploy | Annuelle ou sur depart | +| Backup encryption keys | Conserve hors-bande, rotate seulement si compromise | + +## 8. Pre-deploy checklist (par release) + +``` +[ ] CI vert sur la PR +[ ] Tests E2E staging passent +[ ] CHANGELOG.md a jour +[ ] Migration data documentee si schema change +[ ] Pre-prod rollback test recent (< 1 mois) +[ ] Pas de PR open critique +[ ] Backup recent (< 24h) verifie +[ ] Approval reviewer disponible (Yan ou Corentin) +[ ] Pas de creneau metier critique (cours en cours / saisie deadline) +``` + +## 9. Post-deploy validation + +### 9.1 Smoke tests automatiques (script) + +```bash +# scripts/smoke-test.sh +ENV_URL=$1 # https://wiki.acadenice.fr ou staging + +set -e + +# 1. Healthcheck +curl -fsS "$ENV_URL/api/health" || exit 1 + +# 2. Login admin (test creds) +curl -fsS -X POST -H "Content-Type: application/json" \ + -d '{"email":"smoke@acadenice.fr","password":"..."}' \ + "$ENV_URL/api/auth.email" > /dev/null + +# 3. Lecture page test +curl -fsS "$ENV_URL/api/documents.info" -H "Authorization: Bearer $TOKEN" \ + -d '{"id":"smoke-test-page"}' > /dev/null + +# 4. Test recherche +curl -fsS "$ENV_URL/api/documents.search" -H "Authorization: Bearer $TOKEN" \ + -d '{"query":"smoke"}' > /dev/null + +echo "Smoke tests OK" +``` + +### 9.2 Manual checklist post-deploy + +``` +[ ] Smoke tests automatiques verts +[ ] Login Docmost web OK +[ ] Login Baserow web OK +[ ] Pages wiki recentes accessibles +[ ] Saisie test sur Baserow OK (heures realisees ou intervention) +[ ] Diagrammes Mermaid rendent OK sur une page test +[ ] Logs containers : pas d'erreurs dans les 5 dernieres minutes +[ ] Metriques system : CPU < 50%, RAM < 70% (apres charge initiale) +``` + +### 9.3 Watch period + +30 minutes apres deploy prod : +- Logs surveilles activement +- Metriques uptime monitoring +- Si anomalie : trigger rollback + +## 10. Questions ouvertes + +- [ ] Registry images : GitHub Container Registry, registry.acadenice.fr (a deployer), ou Harbor self-host ? +- [ ] Backup distant : OVH Object Storage / Backblaze / S3 ? Choix selon prix + souverainete +- [ ] Sentry pour error tracking (Phase 3+) ? Self-host ou SaaS ? +- [ ] CI runner : GitHub-hosted (cout) ou self-hosted runner sur VPS Acadenice ? +- [ ] Notification deploy : Slack, Teams, email ? Tous les 3 ? diff --git a/docs/18-plan-operations.md b/docs/18-plan-operations.md new file mode 100644 index 0000000..11595fb --- /dev/null +++ b/docs/18-plan-operations.md @@ -0,0 +1,459 @@ +# Plan d'operations (RUN) + +> Strategie d'operations post-launch : monitoring, alerting, backups, DR, incident response, runbooks. +> Audience : Corentin (owner ops), Yan (backup), futur freelance. + +## 1. Vue d'ensemble — RUN responsibilities + +```mermaid +flowchart TB + subgraph "Daily" + D1[Check uptime monitoring] + D2[Verifier logs erreurs] + D3[Review backups quotidiens] + end + subgraph "Weekly" + W1[Audit dependabot bumps] + W2[Check capacite disque/CPU] + W3[Review issues / PR ops] + end + subgraph "Monthly" + M1[Test restauration backup] + M2[Review security alerts] + M3[Audit access list] + M4[Capacity planning review] + end + subgraph "On Incident" + I1[Detect / Page] + I2[Triage] + I3[Mitigate / Restore] + I4[Post-mortem] + end +``` + +## 2. Monitoring + +### 2.1 Stack de monitoring (Phase 1 minimal → Phase 3 complet) + +| Phase | Tool | Role | Cout | +|-------|------|------|------| +| **Phase 1** | UptimeRobot (free) | Healthcheck HTTP toutes 5 min sur wiki + baserow | 0€ | +| **Phase 2** | + Uptime Kuma self-host | Plus de granularite, dashboards perso | 0€ (sur prod VPS ou VPS dedie) | +| **Phase 3** | + Prometheus + Grafana | Metriques system + app, alerting fin | ~5€/mois (extra resources) | +| **Phase 3** | + Loki | Centralisation logs containers | ~5€/mois | +| **Phase 4** | + Sentry self-host ou SaaS | Error tracking app, stack traces | 0€-25€/mois | + +### 2.2 Endpoints surveilles (Phase 1) + +| Endpoint | Frequence | SLA cible | +|----------|-----------|-----------| +| `https://wiki.acadenice.fr` (HTTP 200) | 5 min | uptime >= 99% | +| `https://baserow.acadenice.fr/api/_health/` | 5 min | uptime >= 99% | +| `https://bridge.acadenice.fr/api/health` (Phase 2+) | 5 min | uptime >= 99% | + +### 2.3 Metriques cles (Phase 3+) + +System : +- CPU usage (alerte > 80% sustained 5 min) +- Memoire (alerte > 85%) +- Disque (alerte > 80%) +- Network in/out + +Application : +- Latence p95 par endpoint (bridge) +- Taux d'erreurs HTTP 5xx (alerte > 1%) +- Throughput requests/sec +- Queue Redis depth (Baserow celery) +- Postgres connections actives (alerte > 80% pool size) + +Business (custom) : +- Nb saisies heures/jour (sentinel : si chute brutale = bug saisie) +- Nb attributions creees/semaine +- Nb projets en cours +- Capacite formateurs depassee (alerte si > 0) + +## 3. Alerting + +### 3.1 Channels + +| Channel | Severite | Cible | +|---------|----------|-------| +| Email Corentin + Yan | Tous niveaux | corentin@acadenice.fr, yan@acadenice.fr | +| Slack/Teams #ops | warning + critical | Canal interne | +| SMS (Twilio ou OVH) | critical seulement | Corentin (oncall principal) | + +### 3.2 Severites + +| Niveau | Definition | Reponse attendue | +|--------|-----------|------------------| +| **CRITICAL** | Service down / data loss en cours | < 15 min | +| **WARNING** | Degradation perf / capacite proche limit | < 4h ouvrees | +| **INFO** | Audit, releases, backups OK | revue hebdo | + +### 3.3 Alertes initiales (Phase 1) + +``` +[CRITICAL] HTTP 5xx > 5% en 5 min → page Corentin +[CRITICAL] Service down (uptime check fail 3x) → page Corentin + Yan +[CRITICAL] Disque > 95% → page +[WARNING] CPU > 80% sustained 10 min → email +[WARNING] Memoire > 85% → email +[WARNING] Capacite formateur depassee → email admin pedagogique +[INFO] Backup quotidien execute (succes/fail) → log + email si fail +``` + +## 4. Backups — strategie 3-2-1 + +**3** copies des donnees, sur **2** supports differents, dont **1** offsite. + +### 4.1 Targets backup + +| Quoi | Frequence | Outil | Local | Distant | +|------|-----------|-------|-------|---------| +| Postgres docmost | Quotidien 03:00 | `pg_dump.gz` | `/opt/formation-hub/backups/local/` | S3-compatible (OVH/Backblaze) | +| Postgres baserow embedded | Quotidien 03:00 | `pg_dump.gz` | idem | idem | +| Docmost files (uploads) | Quotidien 03:00 | `tar.gz` | idem | idem | +| Baserow data dir | Quotidien 03:00 | `tar.gz` | idem | idem | +| `.env.prod` (encrypted) | Sur changement | gpg + push to vault | (none) | Vault hors bande | + +### 4.2 Retention + +| Type | Local | Distant | +|------|-------|---------| +| Quotidien | 30 jours rolling | 90 jours rolling | +| Hebdo (vendredi) | 12 semaines | 12 mois | +| Mensuel (1er) | 12 mois | 5 ans | + +### 4.3 Scripts backup + +`scripts/backup.sh` : +```bash +#!/usr/bin/env bash +set -euo pipefail +DATE=$(date +%Y%m%d-%H%M%S) +BACKUP_DIR=/opt/formation-hub/backups/local +mkdir -p "$BACKUP_DIR" + +cd /opt/formation-hub + +# Postgres docmost +docker compose -f compose.yml -f compose.prod.yml exec -T docmost-db \ + pg_dump -U docmost docmost | gzip > "$BACKUP_DIR/docmost-db-$DATE.sql.gz" + +# Postgres baserow (embedded — exec dans le container baserow) +docker compose -f compose.yml -f compose.prod.yml exec -T baserow \ + pg_dumpall -U postgres | gzip > "$BACKUP_DIR/baserow-db-$DATE.sql.gz" + +# Files +docker compose -f compose.yml -f compose.prod.yml exec -T docmost \ + tar czf - /app/data/storage > "$BACKUP_DIR/docmost-files-$DATE.tar.gz" + +docker compose -f compose.yml -f compose.prod.yml exec -T baserow \ + tar czf - /baserow/data > "$BACKUP_DIR/baserow-data-$DATE.tar.gz" + +# Sync distant via rclone (configure separement) +rclone copy "$BACKUP_DIR/" s3:acadenice-formation-hub-backup/ --include "*-$DATE.*" + +# Retention locale (supprime > 30 jours) +find "$BACKUP_DIR" -type f -mtime +30 -delete +``` + +`/etc/cron.d/formation-hub-backup` : +``` +0 3 * * * corentin /opt/formation-hub/scripts/backup.sh >> /var/log/formation-hub-backup.log 2>&1 +``` + +### 4.4 Test restauration mensuel + +`scripts/restore-test.sh` execute le 1er du mois sur env isole : +1. Provisionne un VPS test ephemere +2. Restore le backup le plus recent +3. Lance smoke tests +4. Verifie integrite (checksum, nb rows) +5. Si fail : alerte CRITICAL + log +6. Detruit le VPS test + +## 5. Disaster recovery + +### 5.1 Scenarios DR + +| Scenario | Probabilite | Impact | Plan | +|----------|-------------|--------|------| +| VPS down (provider issue) | Faible | Service down 0-4h | Attendre provider OU failover manuel vers VPS backup | +| Corruption Postgres | Faible | Data loss < 24h | Restore depuis backup quotidien | +| Compromission complete (rootkit) | Tres faible | Vol de donnees | Wipe + reinstall + restore data + audit complet + RGPD declaration | +| Provider abandonne service | Tres faible | Service migre | Migration vers autre provider, jusqu'a 1 semaine downtime acceptable | +| Erreur humaine (rm -rf) | Moyenne | Variable | Backup quotidien + soft delete in DB | + +### 5.2 RTO / RPO targets (rappel CDC) + +- **RTO** (Recovery Time Objective) : 4h max +- **RPO** (Recovery Point Objective) : 24h max (backup quotidien) + +### 5.3 Plan de DR — etape par etape + +``` +1. DETECT + - Alerte automatique OU report utilisateur + - Confirmer le scope (qui est down ? quoi est perdu ?) + +2. TRIAGE (15 min) + - Severite (CRITICAL / WARNING) + - Notifier Yan + Ludo si CRITICAL + - Annoncer canal #ops + banner status si user-facing + +3. MITIGATE (selon scenario) + - Restore backup + - Failover + - Hotfix + - Rollback + +4. RESTORE + - Verifier integrite donnees (rollups, FK, nb rows) + - Smoke tests + - Notification "back online" + +5. POST-MORTEM (sous 7 jours) + - Timeline + - Root cause + - Action items + - Ajouter au runbook si pattern recurrent +``` + +## 6. Runbooks + +Documentation par incident type. Format standardise : + +``` +# Runbook : + +## Symptomes +- ... + +## Diagnostic +1. Verifier ... +2. Verifier ... + +## Resolution +1. Step +2. Step + +## Prevention future +- ... + +## Rollback / escalade +- ... +``` + +### 6.1 Runbooks Phase 1 (a creer) + +| Runbook | Priorite | +|---------|----------| +| `runbook-docmost-down.md` | Haute | +| `runbook-baserow-down.md` | Haute | +| `runbook-disk-full.md` | Haute | +| `runbook-postgres-corrupted.md` | Haute | +| `runbook-restore-from-backup.md` | Haute | +| `runbook-rotate-secrets.md` | Moyenne | +| `runbook-bump-docmost-version.md` | Moyenne | +| `runbook-bump-baserow-version.md` | Moyenne | +| `runbook-add-new-user.md` | Faible | +| `runbook-renewal-tls.md` | Faible (auto via Traefik) | + +A stocker dans `docs/runbooks/` ou directement sur Outline pour acces rapide en incident. + +## 7. Maintenance + +### 7.1 Bumps dependances + +| Type | Frequence | Process | +|------|-----------|---------| +| Auto via Dependabot (security) | Hebdo | Auto-PR + CI + merge si vert | +| Auto via Dependabot (minor/patch) | Hebdo | Auto-PR + review humaine | +| Major bumps | Manuel | PR dediee + tests E2E + decision business | +| Docmost upstream | Decision manuelle (testing staging) | PR change image tag + test E2E | +| Baserow upstream | idem | idem | +| Postgres major | Annuel max, planifie | Backup + migration + restore + verification | + +### 7.2 OS patches + +| Type | Frequence | +|------|-----------| +| Security patches Debian | Auto via `unattended-upgrades` | +| Major Debian release | Tous les 2-3 ans, planifie | +| Reboot apres kernel patch | Mensuel max, fenetre maintenance | + +### 7.3 Window de maintenance + +Communiquer 48h avant si downtime > 5 min : +- Email a tous les utilisateurs Acadenice +- Banner Docmost / Baserow +- Slack #internal + +Creneau prefere : **dimanche 06:00-08:00 UTC** (zero usage probable). + +## 8. Capacity planning + +### 8.1 Indicateurs a surveiller + +- Nb users actifs (mensuel) +- Volume rows Baserow (par table) +- Volume documents Docmost +- Storage uploads +- CPU/RAM moyenne sur 7 jours + +### 8.2 Triggers d'upsizing + +| Indicateur | Seuil | Action | +|-----------|-------|--------| +| CPU moyen > 60% sur 1 semaine | Trigger | Upsize VPS (4 → 8 vCPU) | +| RAM moyen > 75% sur 1 semaine | Trigger | Upsize RAM (8 → 16 Go) | +| Disque > 70% | Trigger | Upsize storage OU clean old backups | +| Nb users simultanes peak > 50 | Trigger | Considerer 2 replicas + load balancer | + +### 8.3 Review trimestrielle + +Tous les 3 mois, Corentin review : +- Couts infra +- Adequation specs +- Croissance attendue prochain trimestre +- Decision upsize/downsize/migrate + +## 9. Incident response + +### 9.1 Severites (rappel) + +- **SEV1** : Service down complet (CRITICAL) +- **SEV2** : Degradation majeure (WARNING) +- **SEV3** : Bug isole, workaround possible (INFO) + +### 9.2 Comm template + +Pendant incident : +``` +[SEV1] formation-hub - Service degraded +Symptom: +Started: +Investigation: +ETA: +Channel: #ops +``` + +Mise a jour toutes les 30 min. + +### 9.3 Post-mortem template + +`docs/post-mortems/YYYY-MM-DD-titre.md` : + +```markdown +# Post-mortem : + +## Timeline +- HH:MM detection +- HH:MM triage +- HH:MM mitigation start +- HH:MM service restored +- HH:MM root cause confirmed + +## Impact +- Duree downtime : Xh +- Users impactes : Y +- Data loss : oui/non, si oui : combien + +## Root cause +<...> + +## Pourquoi notre monitoring n'a pas alerte plus tot ? +<...> + +## Action items +- [ ] AI 1 : ... (owner @who, due date) +- [ ] AI 2 : ... + +## Lessons learned +<...> +``` + +Post-mortem **blameless** : focus sur le systeme, pas la personne. + +## 10. Daily / Weekly / Monthly tasks + +### 10.1 Daily (5 min, matin) + +``` +[ ] Check uptime monitoring (vert ?) +[ ] Verifier logs containers (pas d'erreur recurrente ?) +[ ] Verifier backup quotidien execute (status email ou log) +[ ] Check Slack #ops (rien d'urgent ?) +``` + +### 10.2 Weekly (30 min, lundi matin) + +``` +[ ] Review Dependabot PRs +[ ] Check disque/CPU graphs (anomalies ?) +[ ] Review issues GitHub ops/sec +[ ] Update CHANGELOG si releases passees +[ ] Plan release prochaine si features pretes +``` + +### 10.3 Monthly (2h, 1er du mois) + +``` +[ ] Test restauration backup (DR exercice) +[ ] Audit access list (qui a acces a quoi ?) +[ ] Review security alerts (CVE, audits) +[ ] Capacity planning review +[ ] Review couts infra (vs budget) +[ ] Update runbooks si nouveaux patterns +[ ] Review monitoring : alertes sur-bruyantes ? sous-detectes ? +``` + +## 11. On-call rotation (futur) + +Pour l'instant : **Corentin = oncall principal**, Yan = backup. + +Si plus d'admin technique embauches plus tard : +- Rotation hebdo Corentin / Yan / N +- Handoff weekly avec recap +- Compensation oncall (jour off ou prime) + +## 12. Communication metier + +Channels : +- **#ops** Slack/Teams : equipe technique +- **#internal** : tous les salaries Acadenice +- **Email all** : announcements majeurs (releases breaking, maintenance) +- **Banner Docmost** : info live downtime / maintenance + +## 13. Documentation des operations + +Tout doit etre dans `docs/runbooks/` (ou Outline `[INTERNE] Runbooks`) : +- Comment faire un backup manuel +- Comment restorer +- Comment ajouter un user +- Comment rotate les secrets +- Comment bump une version Docmost ou Baserow +- Comment investiguer un alert +- Comment escalader un incident + +## 14. Outils ops — recap + +| Outil | Phase | Cout/mois | +|-------|-------|-----------| +| UptimeRobot free | Phase 1+ | 0€ | +| Uptime Kuma self-host | Phase 2+ | 0€ | +| Prometheus + Grafana | Phase 3+ | ~5€ resources | +| Loki | Phase 3+ | ~5€ resources | +| Sentry | Phase 4+ | 0-25€ | +| pg_dump + tar + rclone | Phase 1+ | 0€ | +| OVH Object Storage / Backblaze | Phase 1+ | ~5-10€ | +| Slack / Teams webhook | Phase 1+ | 0€ (existant) | + +## 15. Questions ouvertes + +- [ ] Self-host Uptime Kuma vs SaaS UptimeRobot pour Phase 1 ? +- [ ] Backup distant : OVH (souverainete FR) vs Backblaze (cout) ? +- [ ] On-call rotation et compensation a definir si embauche +- [ ] Runbook execution automatique (Rundeck ?) ou pure markdown ? +- [ ] Status page publique (Statuspage.io / self-host) pour transparence vers users ? diff --git a/docs/19-bridge-api-design.md b/docs/19-bridge-api-design.md new file mode 100644 index 0000000..ebc2eba --- /dev/null +++ b/docs/19-bridge-api-design.md @@ -0,0 +1,536 @@ +# Bridge API Design + +> Specification du **bridge service** : architecture, endpoints, auth, contrats, integration patterns. +> Service Node TS qui expose Baserow comme nodes Tiptap custom dans Docmost et orchestre les rollups cross-zone. +> Statut : design doc avant code Phase 2. + +## 1. Mission du bridge + +Le bridge est notre **seul code custom**. Il a 4 missions : + +1. **Exposer les rows Baserow comme objets typed** au reste de l'ecosysteme (typing strict, validation, cache) +2. **Recevoir les webhooks Baserow** pour invalider caches et declencher actions (notifications, recalculs cross-zone) +3. **Servir les Tiptap node-views custom** dans Docmost (mention `@formateur`, embed `[projet]`, etc.) avec donnees fraiches +4. **Orchestrer les workflows metier** que ni Docmost ni Baserow ne savent faire seuls (validation RG, notifications croisees, capacite Personne unifiee) + +Le bridge **ne stocke pas d'etat metier** (Phase 2). Source of truth = Baserow. Le bridge est stateless avec cache Redis. + +## 2. Tech stack + +| Composant | Choix | Justification | +|-----------|-------|---------------| +| Runtime | Node 22 LTS | Stable, ecosysteme TS mature | +| Framework HTTP | Hono | Leger, performant, TypeScript-first, edge-ready si futur | +| Validation | Zod | Schemas TS-typed, runtime validation | +| HTTP client | ofetch | Wrapper fetch avec retry, timeout, JSON typing | +| Cache | Redis 7 | Partage avec Docmost ou dedie (decision Phase 2) | +| Tests | Vitest + testcontainers | Cf doc 16 | +| Logger | Pino | Structured JSON logs, perf | +| Config | dotenv + zod | `.env` parse + valide au boot | +| Build | TypeScript native + bundling esbuild | Pas Webpack, simple | +| Deploy | Docker image multi-stage | Image < 100 Mo | + +## 3. Architecture interne + +```mermaid +flowchart TB + subgraph "Bridge service (Hono)" + Routes[Routes layer
endpoints REST + webhooks] + Middleware[Middleware
auth, logging, rate-limit, error] + Services[Services layer
PersonneService, ProjetService, etc.] + Adapters[Adapters layer
BaserowClient, DocmostClient, RedisCache] + Domain[Domain layer
Personne, Module, Tache classes pures] + end + + Routes --> Middleware + Middleware --> Services + Services --> Domain + Services --> Adapters + + Adapters -->|HTTP| Baserow[(Baserow API)] + Adapters -->|HTTP| Docmost[(Docmost API)] + Adapters -->|TCP| Redis[(Redis)] +``` + +Layers : +- **Routes** : declaration endpoints + Zod schemas + delegation services +- **Middleware** : transverse (auth, logs, rate-limiting, error handling) +- **Services** : logique metier orchestree (Use Cases level) +- **Domain** : classes pures (Personne, Module, Attribution... cf doc 12) +- **Adapters** : isolation IO (Baserow API, Docmost API, Redis) + +## 4. Conventions API REST + +### 4.1 Style + +- **REST-ish** : endpoints orientes resources, verbes HTTP standards (GET, POST, PUT, PATCH, DELETE) +- **JSON** uniquement (request + response) +- **Response shape standard** : +```typescript +// Success +{ "data": , "meta"?: { ... } } + +// Error +{ "error": { "code": "ERR_CODE", "message": "Human readable", "details"?: {...} } } +``` + +### 4.2 Naming + +- Plural noms : `/personnes`, `/projets`, `/attributions` +- IDs en path : `/personnes/:id` +- Sub-resources : `/projets/:id/taches` +- Actions : verb-style en POST si non-CRUD : `/attributions/:id/cloturer` + +### 4.3 Versioning + +- Prefix `/api/v1/` sur toutes les routes +- Breaking change → nouvelle version `/api/v2/` (en parallele pendant transition) +- Deprecations annoncees minimum 3 mois avant retrait + +### 4.4 Pagination, filtre, tri (pour les list endpoints) + +``` +GET /api/v1/personnes? + page=1& # default 1 + per_page=50& # default 50, max 200 + sort=nom& # default id desc + filter[role]=formateur& + filter[statut]=actif +``` + +Response : +```json +{ + "data": [...], + "meta": { + "page": 1, + "per_page": 50, + "total": 127, + "total_pages": 3 + } +} +``` + +## 5. Authentification + +### 5.1 Strategies + +| Type | Usage | Header | +|------|-------|--------| +| **API Token longue duree** | Service-to-service (Docmost ↔ Bridge, Cron ↔ Bridge) | `Authorization: Bearer brg_` | +| **JWT court** (Phase 3+) | User authentifie via Docmost SSO | `Authorization: Bearer ` | +| **Webhook signature** | Verification webhook Baserow | `X-Baserow-Signature: ` | + +### 5.2 Generation tokens + +API tokens generes via CLI bridge : +```bash +npm run --prefix bridge token:create -- --name "docmost-prod" --scopes "read:* write:attributions" +# → "brg_a1b2c3d4..." stocke en .env.prod cote Docmost +``` + +Tokens stockes en clair dans une table `api_tokens` Postgres (Phase 3+) ou en memoire au boot via `.env` (Phase 2 simple). + +### 5.3 Scopes + +| Scope | Permissions | +|-------|-------------| +| `read:personnes` | GET /personnes/* | +| `read:projets` | GET /projets/* | +| `write:attributions` | POST/PATCH /attributions | +| `write:interventions` | POST/PATCH /interventions | +| `webhook:baserow` | POST /webhooks/baserow/* | +| `admin:*` | Tout (Corentin/Yan tokens) | + +## 6. Endpoints REST + +### 6.1 Personnes + +| Method | Path | Scope | Description | +|--------|------|-------|-------------| +| GET | `/api/v1/personnes` | `read:personnes` | Liste paginee, filtrable par role/statut | +| GET | `/api/v1/personnes/:id` | `read:personnes` | Fiche detail avec heures restantes (formation + agence + total) | +| GET | `/api/v1/personnes/:id/attributions` | `read:personnes` | Attributions actives + historiques | +| GET | `/api/v1/personnes/:id/interventions` | `read:personnes` | Interventions sur taches (paginees) | +| GET | `/api/v1/personnes/:id/dashboard` | `read:personnes` | Vue 360 : capacite, attributions, interventions, projets en cours | + +### 6.2 Formations / Blocs / Modules + +| Method | Path | Scope | Description | +|--------|------|-------|-------------| +| GET | `/api/v1/formations` | `read:formations` | Liste paginee | +| GET | `/api/v1/formations/:id` | `read:formations` | Detail avec blocs/modules + rollups | +| GET | `/api/v1/blocs/:id` | `read:formations` | Detail bloc + modules | +| GET | `/api/v1/modules/:id` | `read:formations` | Detail module + attributions actives | +| POST | `/api/v1/modules/:id/attribuer` | `write:attributions` | Cree une attribution avec validation RG | + +### 6.3 Attributions + +| Method | Path | Scope | Description | +|--------|------|-------|-------------| +| GET | `/api/v1/attributions/:id` | `read:attributions` | Detail | +| PATCH | `/api/v1/attributions/:id/heures-realisees` | `write:attributions` | Saisir heures realisees (UC-13) | +| POST | `/api/v1/attributions/:id/cloturer` | `write:attributions` | Statut → realise | +| POST | `/api/v1/attributions/:id/annuler` | `write:attributions` | Statut → annule (justification requise) | + +### 6.4 Clients / Projets / Taches + +| Method | Path | Scope | Description | +|--------|------|-------|-------------| +| GET | `/api/v1/clients` | `read:projets` | Liste | +| GET | `/api/v1/clients/:id` | `read:projets` | Detail + projets | +| GET | `/api/v1/projets` | `read:projets` | Liste filtrable par statut/client | +| GET | `/api/v1/projets/:id` | `read:projets` | Detail + taches + heures realisees rollup | +| GET | `/api/v1/projets/:id/timeline` | `read:projets` | Vue chronologique interventions | +| GET | `/api/v1/taches/:id` | `read:projets` | Detail + interventions | + +### 6.5 Interventions + +| Method | Path | Scope | Description | +|--------|------|-------|-------------| +| POST | `/api/v1/interventions` | `write:interventions` | Saisir intervention (UCA-07) | +| PATCH | `/api/v1/interventions/:id` | `write:interventions` | Edit (heures, notes) | +| POST | `/api/v1/interventions/:id/annuler` | `write:interventions` | Annulation | + +### 6.6 Rapports + +| Method | Path | Scope | Description | +|--------|------|-------|-------------| +| GET | `/api/v1/rapports/formation/:id?format=pdf` | `read:formations` | PDF rapport formation | +| GET | `/api/v1/rapports/personne/:id?format=pdf` | `read:personnes` | PDF rapport personne (heures + attributions) | +| GET | `/api/v1/rapports/projet/:id?format=pdf` | `read:projets` | PDF rapport projet | + +### 6.7 Health & metrics + +| Method | Path | Scope | Description | +|--------|------|-------|-------------| +| GET | `/api/health` | (none) | Healthcheck (200 si OK, 503 si degraded) | +| GET | `/api/ready` | (none) | Readiness (Baserow + Redis joignables) | +| GET | `/api/metrics` | `admin:*` | Prometheus metrics format | + +## 7. Webhooks Baserow + +Baserow envoie des webhooks sur les changements de rows. Bridge traite et reagit. + +### 7.1 Endpoints webhook + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/api/webhooks/baserow/attribution-changed` | Row created/updated/deleted sur table `attribution` | +| POST | `/api/webhooks/baserow/intervention-changed` | Idem `intervention` | +| POST | `/api/webhooks/baserow/module-status-changed` | Quand `module_statut` change | +| POST | `/api/webhooks/baserow/projet-status-changed` | Quand `projet_statut` change | + +### 7.2 Format payload Baserow (extrait) + +```json +{ + "table_id": 123, + "database_id": 1, + "event_type": "rows.created", + "items": [ + { "id": 42, "field_X": "...", ... } + ] +} +``` + +### 7.3 Verification signature + +```typescript +// middleware/webhook-baserow.ts +const expected = hmacSha256(rawBody, env.BASEROW_WEBHOOK_SECRET); +const provided = req.headers['X-Baserow-Signature']; +if (!constantTimeEqual(expected, provided)) { + throw new Error('Invalid signature'); +} +``` + +### 7.4 Actions par webhook + +| Webhook | Actions | +|---------|---------| +| attribution-changed | Invalide cache Redis personne/module concernes ; si statut change vers `realise` ou `annule` → recalcul rollup module ; notif email formateur si nouvelle attribution | +| intervention-changed | Invalide cache personne/tache ; check capacite Personne, alerte si depassement | +| module-status-changed | Si tous modules d'une formation `realise` → declenche cloture formation auto (OP-07) | +| projet-status-changed | Si `livre` → notif admin pour facturation | + +### 7.5 Idempotence + +Chaque webhook a un `event_id`. Le bridge stocke en Redis avec TTL 24h les events deja traites : +```typescript +const seen = await redis.get(`webhook:event:${event_id}`); +if (seen) return; // skip duplicate +await redis.set(`webhook:event:${event_id}`, '1', 'EX', 86400); +``` + +## 8. Cache strategy + +### 8.1 Cles Redis + +| Cle | TTL | Contenu | +|-----|-----|---------| +| `bridge:personne:` | 5 min | JSON full Personne (avec rollups calcules) | +| `bridge:projet:` | 5 min | JSON full Projet | +| `bridge:formation:` | 10 min | JSON Formation (change moins souvent) | +| `bridge:webhook:event:` | 24h | Idempotence webhook | +| `bridge:rate-limit::` | 1 min | Rate limit counter | + +### 8.2 Invalidation + +- **Webhook Baserow** invalide les cles concernees +- **TTL** comme fallback (5-10 min) +- Pattern : `cache.invalidate('bridge:personne:42')` apres write + +### 8.3 Cache aside pattern + +```typescript +async getPersonne(id: number): Promise { + const cached = await cache.get(`bridge:personne:${id}`); + if (cached) return Personne.fromJSON(cached); + + const fresh = await baserow.fetchPersonne(id); + await cache.set(`bridge:personne:${id}`, fresh.toJSON(), 'EX', 300); + return fresh; +} +``` + +## 9. Rate limiting + +Par token + endpoint (sliding window 1 min) : + +| Endpoint | Limite | +|----------|--------| +| Read endpoints | 600 req/min | +| Write endpoints | 60 req/min | +| Webhooks | 1000 req/min | +| Rapports PDF | 10 req/min | + +Reponse 429 si depasse : +```json +{ "error": { "code": "RATE_LIMITED", "message": "Too many requests", "retry_after": 30 } } +``` + +## 10. Error handling + +### 10.1 Codes d'erreur + +| Code | HTTP | Description | +|------|------|-------------| +| `AUTH_REQUIRED` | 401 | Token absent | +| `AUTH_INVALID` | 401 | Token invalide | +| `FORBIDDEN_SCOPE` | 403 | Token n'a pas le scope requis | +| `NOT_FOUND` | 404 | Ressource inexistante | +| `VALIDATION_ERROR` | 400 | Body invalide (Zod errors) | +| `RG_VIOLATION` | 422 | Regle de gestion violee (ex RG-01 depassement heures module) | +| `CONFLICT` | 409 | Etat incoherent (ex annuler une attribution deja annulee) | +| `RATE_LIMITED` | 429 | Trop de requetes | +| `BASEROW_UNAVAILABLE` | 502 | Baserow API down | +| `INTERNAL` | 500 | Bug bridge | + +### 10.2 Format + +```json +{ + "error": { + "code": "RG_VIOLATION", + "message": "Heures attribuees depassent la capacite du module", + "details": { + "rule": "RG-01", + "module_id": 42, + "heures_module": 30, + "heures_deja_attribuees": 28, + "heures_demandees": 5 + } + } +} +``` + +## 11. Integration patterns Docmost + +### 11.1 Tiptap node-view custom + +Phase 2+ : on developpe (ou on commande a un freelance) des extensions Tiptap pour Docmost qui appellent le bridge. + +Patterns : +- **Mention** `@formateur:Pierre` → render carte avec capacite restante via GET /personnes/:id (slug → resolution) +- **Embed** `[projet:projet-alpha]` → render card avec status + heures realisees +- **Database view** `[modules-a-attribuer]` → embed kanban filtré + +Ces nodes appellent le bridge via fetch + cache cote client (5 min). + +### 11.2 Routes pages full + +Phase 2+ : le bridge sert aussi des **pages full** /personne/:id, /projet/:id, /formation/:id qui ressemblent a des pages Docmost (header layout + content). + +Implementation : Hono cote backend rend HTML avec layout Docmost mimique + content custom. Le user clique sur une mention dans Docmost, ouvre la page bridge, voit le meme look. + +Ou en Phase 3 : on contribue au repo Docmost upstream pour ajouter ces nodes nativement. + +## 12. Sample request/response + +### Saisir heures realisees + +```http +PATCH /api/v1/attributions/42/heures-realisees HTTP/1.1 +Host: bridge.acadenice.fr +Authorization: Bearer brg_xxxxx +Content-Type: application/json + +{ + "heures_realisees": 3.5, + "comment": "Cours JS du 2026-05-07 OK" +} +``` + +Response 200 : +```json +{ + "data": { + "attribution_id": 42, + "heures_attribuees": 10, + "heures_realisees": 3.5, + "statut": "en_cours", + "module": { + "module_id": 17, + "module_nom": "JS Fondamentaux", + "heures_realisees_total": 3.5 + }, + "personne": { + "personne_id": 5, + "nom_prenom": "Pierre Dupont", + "heures_attribuees_formation": 80, + "heures_restantes_formation": 670 + } + } +} +``` + +### Erreur RG violation + +```http +POST /api/v1/modules/17/attribuer HTTP/1.1 +Authorization: Bearer brg_xxxxx +Content-Type: application/json + +{ + "personne_id": 5, + "heures_attribuees": 50, + "date_debut": "2026-09-01" +} +``` + +Response 422 : +```json +{ + "error": { + "code": "RG_VIOLATION", + "message": "Heures attribuees depassent la capacite du module", + "details": { + "rule": "RG-01", + "module_id": 17, + "heures_module": 30, + "heures_deja_attribuees": 0, + "heures_demandees": 50 + } + } +} +``` + +## 13. Observabilite + +### 13.1 Logs (Pino structured JSON) + +Niveau `info` par defaut, `debug` en local. Format : +```json +{ + "level": "info", + "time": "2026-05-07T10:23:45.123Z", + "msg": "PATCH /api/v1/attributions/42/heures-realisees", + "method": "PATCH", + "path": "/api/v1/attributions/42/heures-realisees", + "status": 200, + "duration_ms": 142, + "user_token_id": "tok_abc123", + "request_id": "req_xyz789" +} +``` + +Champs sensibles redactes : pas de body en logs, pas de token en clair. + +### 13.2 Metrics Prometheus + +Exposees sur `/api/metrics` : +- `http_requests_total{method,path,status}` counter +- `http_request_duration_seconds{method,path}` histogram +- `baserow_api_calls_total{endpoint,status}` counter +- `cache_hits_total` / `cache_misses_total` +- `webhook_events_processed_total{type,outcome}` + +## 14. Tests + +Cf doc 16 plan-de-tests : +- Unit Vitest 80% coverage minimum sur domain +- Integration tests avec testcontainers Baserow + Redis +- E2E playwright sur staging + +## 15. Roadmap implementation + +### Phase 2.0 — Bootstrap (semaine 1-2) + +- [ ] Setup Hono + zod + ofetch + pino +- [ ] BaserowClient avec tests integration +- [ ] DocmostClient skeleton +- [ ] Healthcheck endpoint +- [ ] Auth middleware basique (API token) +- [ ] CI/CD complet (cf doc 17) +- [ ] Deploy staging + +### Phase 2.1 — Read endpoints (semaine 3-4) + +- [ ] GET /personnes/:id avec rollups calcules +- [ ] GET /projets/:id +- [ ] GET /formations/:id +- [ ] Cache Redis pattern cache-aside +- [ ] Tests integration sur les endpoints + +### Phase 2.2 — Write endpoints + webhooks (semaine 5-7) + +- [ ] POST /interventions +- [ ] PATCH /attributions/:id/heures-realisees +- [ ] Webhooks Baserow handlers +- [ ] Validation RG-01 a RG-06 +- [ ] Tests integration write + +### Phase 2.3 — Tiptap nodes (semaine 8-10) + +- [ ] Premier node Tiptap custom (mention `@formateur`) +- [ ] Integration Docmost (fork ou plugin) +- [ ] E2E playwright + +### Phase 2.4 — Pages full + rapports (semaine 11-12) + +- [ ] Routes /personne/:id, /projet/:id en page Docmost-style +- [ ] Endpoint /rapports/* PDF generation +- [ ] Stabilisation, fix bugs, doc utilisateur + +## 16. Decisions a prendre + +- [ ] **Source of truth tokens** : .env (simple) vs Postgres dedie (rotation a chaud) ? Mon vote : .env Phase 2, Postgres Phase 3 +- [ ] **Cache Redis partage Docmost ou dedie** ? Partage Phase 2 (simple, sa marche), dedie Phase 3 si charge ou conflits +- [ ] **PDF generation** : Puppeteer (lourd) vs PDFKit (manuel) vs service externe (gotenberg) ? Recommande PDFKit ou gotenberg self-host +- [ ] **OpenAPI 3 doc auto** : generee depuis Zod schemas ? Lib `@asteasolutions/zod-to-openapi`. A faire Phase 2.1. +- [ ] **GraphQL au lieu de REST ?** Pas pertinent pour notre scope (peu de endpoints, peu de variation queries). REST est plus simple. +- [ ] **Multi-tenant** ? Pour l'instant non — Acadenice mono-instance. Si rachat / scaling : ajouter `tenant_id` partout. Pas avant Phase 4. + +## 17. Glossaire + +| Terme | Definition | +|-------|------------| +| Bridge | Service custom qui se sert d'intermediaire entre Docmost (UI) et Baserow (data) | +| Tiptap node-view | Composant React custom integre dans editeur Tiptap pour rendre un block specifique | +| Cache aside | Pattern : check cache → if miss, fetch source + populate cache | +| Idempotence | Une requete repetee a le meme effet qu'une requete unique (anti-doublon) | +| HMAC signature | Hash crypto pour verifier l'authenticite d'un payload (webhook) | +| Sliding window rate limit | Compteur sur fenetre glissante (ex: derniere minute) | +| RG | Regle de Gestion (Merise) | +| Scope (token) | Permission specifique (read:X, write:Y, admin:*) | diff --git a/docs/diagrams/README.md b/docs/diagrams/README.md new file mode 100644 index 0000000..ca3be2c --- /dev/null +++ b/docs/diagrams/README.md @@ -0,0 +1,20 @@ +# Diagrammes drawIO + +Fichiers `.drawio` (XML). Ouvrir dans : +- [app.diagrams.net](https://app.diagrams.net) (web) +- Docmost natif (block drawIO, depuis v0.3.0) +- VS Code extension `Drawio Integration` ([Henning Dieterichs](https://marketplace.visualstudio.com/items?itemName=hediet.vscode-drawio)) + +## Liste + +| Fichier | Description | Lien Outline (XML a importer) | +|---------|-------------|-------------------------------| +| `architecture-infra.drawio` | Vue archi infra complete (Traefik + Docmost + Baserow + Bridge + storage + ops) | A pousser dans Outline | + +## Import dans diagrams.net + +1. Aller sur https://app.diagrams.net +2. Choisir "Open Existing Diagram" +3. Selectionner le fichier `.drawio` local OU coller le XML via `Extras → Edit Diagram (XML)` +4. Polir le layout si besoin (`Layout → Vertical Tree Layout` peut aider) +5. Sauver, exporter en SVG/PNG si besoin diff --git a/docs/diagrams/architecture-infra.drawio b/docs/diagrams/architecture-infra.drawio new file mode 100644 index 0000000..71ff0f6 --- /dev/null +++ b/docs/diagrams/architecture-infra.drawio @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100755 index 0000000..95f0cfd --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# scripts/backup.sh — backup quotidien Postgres + files (a appeler par cron) +set -euo pipefail + +DATE=$(date +%Y%m%d-%H%M%S) +BACKUP_DIR="${BACKUP_DIR:-/opt/formation-hub/backups/local}" +COMPOSE_FILES="${COMPOSE_FILES:--f compose.yml -f compose.prod.yml}" +RETENTION_DAYS="${RETENTION_DAYS:-30}" + +mkdir -p "$BACKUP_DIR" + +echo "[$(date -Iseconds)] Backup start — DATE=$DATE" + +cd "$(dirname "$0")/.." + +echo " Postgres docmost..." +docker compose $COMPOSE_FILES exec -T docmost-db \ + pg_dump -U docmost docmost | gzip > "$BACKUP_DIR/docmost-db-$DATE.sql.gz" + +echo " Baserow data..." +docker compose $COMPOSE_FILES exec -T baserow \ + tar czf - /baserow/data > "$BACKUP_DIR/baserow-data-$DATE.tar.gz" + +echo " Docmost files..." +docker compose $COMPOSE_FILES exec -T docmost \ + tar czf - /app/data/storage > "$BACKUP_DIR/docmost-files-$DATE.tar.gz" + +echo " Sync distant (rclone) — si configure..." +if command -v rclone >/dev/null 2>&1 && [ -n "${RCLONE_REMOTE:-}" ]; then + rclone copy "$BACKUP_DIR/" "$RCLONE_REMOTE:" --include "*-$DATE.*" +else + echo " (rclone non configure — backup distant skip)" +fi + +echo " Cleanup local > ${RETENTION_DAYS}j..." +find "$BACKUP_DIR" -type f -mtime "+$RETENTION_DAYS" -delete + +echo "[$(date -Iseconds)] Backup OK" +ls -lh "$BACKUP_DIR/"*-$DATE.* diff --git a/scripts/healthcheck.sh b/scripts/healthcheck.sh new file mode 100755 index 0000000..0d63bde --- /dev/null +++ b/scripts/healthcheck.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# scripts/healthcheck.sh — verifie que la stack repond +set -euo pipefail + +DOCMOST_URL="${DOCMOST_URL:-http://localhost:3000}" +BASEROW_URL="${BASEROW_URL:-http://localhost:8080}" +BRIDGE_URL="${BRIDGE_URL:-}" +TIMEOUT="${HEALTHCHECK_TIMEOUT:-10}" + +red() { printf '\033[31m%s\033[0m\n' "$1"; } +green() { printf '\033[32m%s\033[0m\n' "$1"; } + +check() { + local name="$1" + local url="$2" + if curl -sf --max-time "$TIMEOUT" -o /dev/null "$url"; then + green " OK $name : $url" + return 0 + else + red " KO $name : $url" + return 1 + fi +} + +echo "Healthcheck (timeout ${TIMEOUT}s)..." + +ok=0 +total=0 + +((++total)) || true +check "Docmost " "$DOCMOST_URL" && ((++ok)) || true + +((++total)) || true +check "Baserow " "$BASEROW_URL" && ((++ok)) || true + +if [ -n "$BRIDGE_URL" ]; then + ((++total)) || true + check "Bridge " "$BRIDGE_URL/api/health" && ((++ok)) || true +fi + +echo "" +if [ "$ok" -eq "$total" ]; then + green "Healthcheck : $ok/$total OK" + exit 0 +else + red "Healthcheck : $ok/$total OK" + exit 1 +fi diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100755 index 0000000..d52e58c --- /dev/null +++ b/scripts/smoke-test.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# scripts/smoke-test.sh — test post-deploy minimal +set -euo pipefail + +ENV_URL="${1:-${SMOKE_URL:-http://localhost:3000}}" +TIMEOUT=10 + +echo "Smoke test against $ENV_URL" + +# 1. Healthcheck +echo " [1/3] HEAD $ENV_URL" +curl -sfI --max-time $TIMEOUT "$ENV_URL" > /dev/null + +# 2. Resolve auth.info (assumes Outline-style API) +if [ -n "${SMOKE_AUTH_TOKEN:-}" ]; then + echo " [2/3] auth.info" + curl -sf -X POST --max-time $TIMEOUT \ + -H "Authorization: Bearer $SMOKE_AUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{}' "$ENV_URL/api/auth.info" > /dev/null +else + echo " [2/3] auth.info — SKIP (SMOKE_AUTH_TOKEN absent)" +fi + +# 3. Search +if [ -n "${SMOKE_AUTH_TOKEN:-}" ]; then + echo " [3/3] documents.search" + curl -sf -X POST --max-time $TIMEOUT \ + -H "Authorization: Bearer $SMOKE_AUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"query":"smoke"}' "$ENV_URL/api/documents.search" > /dev/null +fi + +echo "Smoke test OK"