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)
This commit is contained in:
Corentin JOGUET 2026-05-07 12:16:19 +02:00
commit 668576cdc4
55 changed files with 7986 additions and 0 deletions

27
.editorconfig Normal file
View file

@ -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

14
.env.example Normal file
View file

@ -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=

25
.github/CODEOWNERS vendored Normal file
View file

@ -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

51
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,51 @@
---
name: Bug report
about: Signaler un bug
title: "[BUG] "
labels: bug
assignees: Imugiii
---
## Description
<!-- Quoi -->
## 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
<!-- Coller les logs pertinents (sans secrets) ou screenshots -->
```
<logs ici>
```
## Severite
- [ ] CRITICAL (service down / data loss)
- [ ] HIGH (degradation majeure)
- [ ] MEDIUM (bug fonctionnel avec workaround)
- [ ] LOW (cosmetic, edge case)
## Notes additionnelles
<!-- Cas reproductible ? Frequence ? Premiere occurrence ? -->

View file

@ -0,0 +1,41 @@
---
name: Feature request
about: Proposer une nouvelle fonctionnalite
title: "[FEAT] "
labels: enhancement
---
## Probleme metier
<!-- Quel probleme cette feature resout ? -->
## Solution proposee
<!-- Comment on resout ? -->
## Alternatives considerees
<!-- Autres options envisagees et pourquoi elles ont ete ecartees -->
## Impact estime
- Effort dev : <S / M / L / XL>
- Valeur metier : <faible / moyenne / haute / critique>
- Phase visee : <Phase 1 / 2 / 3 / 4>
## Acceptance criteria
```gherkin
Scenario: ...
Given ...
When ...
Then ...
```
## UC concerne(s)
<!-- Reference a doc 11-uml-use-cases.md, ex: UC-13, UCA-07 -->
## Notes additionnelles
<!-- Mockups, screenshots, references -->

37
.github/ISSUE_TEMPLATE/security.md vendored Normal file
View file

@ -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
<!-- Quoi -->
### Risk assessment
- CVSS score estime :
- Vector :
- Impact si exploite :
### Recommandation
<!-- Comment fixer -->
### References
<!-- CVE, CWE, OWASP, etc. -->

39
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,39 @@
## Description
<!-- Que fait cette PR et pourquoi ? -->
## 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 #<num>
## 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
<!-- Points d'attention, decisions architecturales, alternatives considerees -->

60
.github/dependabot.yml vendored Normal file
View file

@ -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"

145
.github/workflows/ci.yml vendored Normal file
View file

@ -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

81
.github/workflows/deploy-prod.yml vendored Normal file
View file

@ -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

57
.github/workflows/deploy-staging.yml vendored Normal file
View file

@ -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

50
.gitignore vendored Normal file
View file

@ -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

32
CHANGELOG.md Normal file
View file

@ -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)

123
CONTRIBUTING.md Normal file
View file

@ -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 : `<type>/<description-kebab>`
- 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 : `<type>(<scope>): <description>`
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

661
LICENSE Normal file
View file

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 <https://www.gnu.org/licenses/>.
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
<https://www.gnu.org/licenses/>.

82
Makefile Normal file
View file

@ -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

79
README.md Normal file
View file

@ -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.

57
SECURITY.md Normal file
View file

@ -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.

31
bridge/.env.example Normal file
View file

@ -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

9
bridge/.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
node_modules/
dist/
coverage/
.env
.env.local
.env.*.local
*.log
.DS_Store
.vitest/

40
bridge/Dockerfile Normal file
View file

@ -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"]

43
bridge/biome.json Normal file
View file

@ -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"
}
}
}

43
bridge/package.json Normal file
View file

@ -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"
}
}

32
bridge/src/index.ts Normal file
View file

@ -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');
});

41
bridge/src/lib/config.ts Normal file
View file

@ -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<typeof ConfigSchema>;
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;
}

23
bridge/src/lib/logger.ts Normal file
View file

@ -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]',
},
});

0
bridge/tests/.gitkeep Normal file
View file

25
bridge/tsconfig.json Normal file
View file

@ -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"]
}

65
compose.prod.yml Normal file
View file

@ -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

44
compose.staging.yml Normal file
View file

@ -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)

76
compose.yml Normal file
View file

@ -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:

View file

@ -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)

View file

@ -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.

141
docs/03-decision-record.md Normal file
View file

@ -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

View file

@ -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<br/>Admin/Formateur/Dev/Etudiant/Client])
subgraph "Edge / Reverse Proxy"
Traefik[Traefik<br/>TLS Let's Encrypt<br/>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<br/>NestJS + React + Tiptap]
Baserow[Baserow<br/>Django + Caddy interne]
Bridge[Bridge service<br/>Node 22 + Hono<br/>Phase 2]
end
subgraph "Storage"
DocmostDB[(Postgres<br/>docmost)]
DocmostRedis[(Redis<br/>docmost)]
BaserowDB[(Postgres<br/>baserow embedded)]
BaserowRedis[(Redis<br/>baserow embedded)]
FS[Local FS / MinIO<br/>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<br/>backups quotidiens]
Monitoring[Uptime monitoring<br/>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)

220
docs/05-data-dictionary.md Normal file
View file

@ -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.

234
docs/06-merise-mcd.md Normal file
View file

@ -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) ?

359
docs/07-merise-mld.md Normal file
View file

@ -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.

226
docs/08-merise-mct.md Normal file
View file

@ -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:<br/>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<br/>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 ?

186
docs/09-merise-mot.md Normal file
View file

@ -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<br/>natif]
end
subgraph "Bridge service"
OP04[OP-04 Saisie heures<br/>UI mobile]
OP07[OP-07 Cloturer formation<br/>cron]
OP09[OP-09 Notifier depassement<br/>webhook + SMTP]
OP11[OP-11 Rapport formation<br/>PDF]
OP12[OP-12 Rapport formateur<br/>PDF]
end
subgraph "Docmost"
OP13[OP-13 Inviter client<br/>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

202
docs/10-state-diagrams.md Normal file
View file

@ -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) ?

240
docs/11-uml-use-cases.md Normal file
View file

@ -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)

View file

@ -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<Role>,
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**.

View file

@ -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<br/>statut != annule?}
A2 -->|Non| Err1([Erreur: module invalide])
A2 -->|Oui| A3[Lister formateurs avec capacite >= heures requises]
A3 --> A4{Au moins 1<br/>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 +<br/>existantes <= heures module?}
A7 -->|Non| Err3([RG-01 KO: depassement heures module])
A7 -->|Oui| A8{Heures + capacite<br/>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<br/><= 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<br/>== 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<br/>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<br/>+ 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<br/>capacite agence restante]
D5 --> D6[INTERVENTION: lier dev a tache, heures]
D6 --> D7{Dev = formateur aussi?}
D7 -->|Oui| D8[Verifier capacite totale<br/>= 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<br/>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:<br/>50 pct formation, 50 pct agence<br/>configurable par personne]
E2 --> E3{Allocation cours<br/>ou projet?}
E3 -->|Cours formation| E4[heures_cours_attribuees<br/>+= 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<br/>+= nouvelles heures]
E7 --> E8{capacite_agence_restante >= 0?}
E8 -->|Non| Err2[Bloquer ou warning]
E8 -->|Oui| E9[OK]
E6 --> Final[Recalculer:<br/>capacite_totale_restante = X - cours_attribuees - projet_attribuees]
E9 --> Final
Final --> End([Affichage tableau de bord<br/>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) |

View file

@ -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 | `<type>/<description-kebab>` 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<MAJOR>.<MINOR>.<PATCH>` (semver) declenche deploy prod |
## 4. Convention de commits
Format : `<type>(<scope>): <description>`
| 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
<!-- Quoi et pourquoi -->
## 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
<!-- Quoi -->
## 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
<!-- Quoi resoudre -->
## Solution proposee
<!-- Comment -->
## Alternatives considerees
<!-- Autres options -->
## Impact estime
<!-- Effort + valeur -->
```
### 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 ?

428
docs/15-baserow-mpd.md Normal file
View file

@ -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/<TABLE_ID>/?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.

259
docs/16-plan-tests.md Normal file
View file

@ -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 <token>` 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

500
docs/17-plan-deployment.md Normal file
View file

@ -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<br/>make up<br/>fixtures seed] --> Push[git push origin main]
Push -->|auto| Staging[Staging<br/>wiki.staging.acadenice.fr<br/>data anonymisee]
Staging --> Tag[git tag v1.X.Y]
Tag -->|approval review| Prod[Prod<br/>wiki.acadenice.fr<br/>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@<vps-ip>
# 2. Hardening de base
adduser corentin --gecos ""
usermod -aG sudo,docker corentin
ssh-copy-id corentin@<vps-ip>
# 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 ?

459
docs/18-plan-operations.md Normal file
View file

@ -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 : <INCIDENT_TYPE>
## 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: <quoi>
Started: <quand>
Investigation: <where we are>
ETA: <estimate restore>
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 : <titre incident>
## 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 ?

View file

@ -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<br/>endpoints REST + webhooks]
Middleware[Middleware<br/>auth, logging, rate-limit, error]
Services[Services layer<br/>PersonneService, ProjetService, etc.]
Adapters[Adapters layer<br/>BaserowClient, DocmostClient, RedisCache]
Domain[Domain layer<br/>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": <payload>, "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_<token>` |
| **JWT court** (Phase 3+) | User authentifie via Docmost SSO | `Authorization: Bearer <jwt>` |
| **Webhook signature** | Verification webhook Baserow | `X-Baserow-Signature: <hmac-sha256>` |
### 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:<id>` | 5 min | JSON full Personne (avec rollups calcules) |
| `bridge:projet:<id>` | 5 min | JSON full Projet |
| `bridge:formation:<id>` | 10 min | JSON Formation (change moins souvent) |
| `bridge:webhook:event:<event_id>` | 24h | Idempotence webhook |
| `bridge:rate-limit:<token>:<endpoint>` | 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<Personne> {
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:*) |

20
docs/diagrams/README.md Normal file
View file

@ -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

View file

@ -0,0 +1,110 @@
<mxfile host="app.diagrams.net" version="24.0.0">
<diagram name="Architecture formation-hub" id="archi-infra">
<mxGraphModel dx="1200" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="user" value="Utilisateur final&#10;(Admin / Formateur / Dev / Etudiant / Client)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="430" y="40" width="320" height="60" as="geometry" />
</mxCell>
<mxCell id="traefik-group" value="Edge / Reverse Proxy" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="380" y="150" width="420" height="80" as="geometry" />
</mxCell>
<mxCell id="traefik" value="Traefik&#10;TLS Let's Encrypt&#10;routing par sous-domaine" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="490" y="180" width="200" height="40" as="geometry" />
</mxCell>
<mxCell id="app-group" value="Application services" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#82b366;dashed=1;verticalAlign=top;fontStyle=1;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="60" y="280" width="1060" height="120" as="geometry" />
</mxCell>
<mxCell id="docmost" value="Docmost&#10;NestJS + React + Tiptap" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="120" y="320" width="220" height="60" as="geometry" />
</mxCell>
<mxCell id="baserow" value="Baserow&#10;Django + Caddy interne" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="480" y="320" width="220" height="60" as="geometry" />
</mxCell>
<mxCell id="bridge" value="Bridge service&#10;Node 22 + Hono&#10;(Phase 2)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;dashed=1;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="840" y="310" width="220" height="80" as="geometry" />
</mxCell>
<mxCell id="storage-group" value="Storage" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=1;verticalAlign=top;fontStyle=1;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="60" y="450" width="1060" height="130" as="geometry" />
</mxCell>
<mxCell id="docmost-db" value="Postgres&#10;docmost" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="100" y="490" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="docmost-redis" value="Redis&#10;docmost" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="260" y="490" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="baserow-db" value="Postgres&#10;baserow (embedded)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="420" y="490" width="170" height="70" as="geometry" />
</mxCell>
<mxCell id="baserow-redis" value="Redis&#10;baserow (embedded)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="630" y="490" width="170" height="70" as="geometry" />
</mxCell>
<mxCell id="fs" value="Local FS / MinIO&#10;docmost files" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="850" y="490" width="160" height="70" as="geometry" />
</mxCell>
<mxCell id="ops-group" value="Infra ops" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;fontStyle=1;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="60" y="620" width="600" height="120" as="geometry" />
</mxCell>
<mxCell id="cron" value="Cron host&#10;backups quotidiens 03:00&#10;pg_dump + tar" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="100" y="660" width="240" height="60" as="geometry" />
</mxCell>
<mxCell id="monitoring" value="Uptime monitoring&#10;(a definir)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;dashed=1;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="380" y="660" width="240" height="60" as="geometry" />
</mxCell>
<mxCell id="e-user-traefik" value="HTTPS" style="endArrow=classic;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontSize=10;" edge="1" parent="1" source="user" target="traefik">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-traefik-docmost" value="wiki.acadenice.fr" style="endArrow=classic;html=1;fontSize=10;" edge="1" parent="1" source="traefik" target="docmost">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-traefik-baserow" value="baserow.acadenice.fr" style="endArrow=classic;html=1;fontSize=10;" edge="1" parent="1" source="traefik" target="baserow">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-traefik-bridge" value="bridge.acadenice.fr" style="endArrow=classic;html=1;fontSize=10;" edge="1" parent="1" source="traefik" target="bridge">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-docmost-db" style="endArrow=classic;html=1;" edge="1" parent="1" source="docmost" target="docmost-db">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-docmost-redis" style="endArrow=classic;html=1;" edge="1" parent="1" source="docmost" target="docmost-redis">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-docmost-fs" style="endArrow=classic;html=1;" edge="1" parent="1" source="docmost" target="fs">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-baserow-db" style="endArrow=classic;html=1;" edge="1" parent="1" source="baserow" target="baserow-db">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-baserow-redis" style="endArrow=classic;html=1;" edge="1" parent="1" source="baserow" target="baserow-redis">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-bridge-baserow" value="API REST" style="endArrow=classic;html=1;fontSize=10;" edge="1" parent="1" source="bridge" target="baserow">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-bridge-docmost" value="API REST" style="endArrow=classic;html=1;fontSize=10;" edge="1" parent="1" source="bridge" target="docmost">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-bridge-redis" value="cache" style="endArrow=classic;html=1;dashed=1;fontSize=10;" edge="1" parent="1" source="bridge" target="docmost-redis">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-cron-docmost-db" value="pg_dump" style="endArrow=classic;html=1;dashed=1;fontSize=10;" edge="1" parent="1" source="cron" target="docmost-db">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-cron-baserow-db" value="pg_dump" style="endArrow=classic;html=1;dashed=1;fontSize=10;" edge="1" parent="1" source="cron" target="baserow-db">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-cron-fs" value="tar" style="endArrow=classic;html=1;dashed=1;fontSize=10;" edge="1" parent="1" source="cron" target="fs">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

39
scripts/backup.sh Executable file
View file

@ -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.*

48
scripts/healthcheck.sh Executable file
View file

@ -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

34
scripts/smoke-test.sh Executable file
View file

@ -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"