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:
commit
668576cdc4
55 changed files with 7986 additions and 0 deletions
27
.editorconfig
Normal file
27
.editorconfig
Normal 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
14
.env.example
Normal 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
25
.github/CODEOWNERS
vendored
Normal 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
51
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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 ? -->
|
||||
41
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
41
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
37
.github/ISSUE_TEMPLATE/security.md
vendored
Normal 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
39
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
60
.github/dependabot.yml
vendored
Normal 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
145
.github/workflows/ci.yml
vendored
Normal 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
81
.github/workflows/deploy-prod.yml
vendored
Normal 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
57
.github/workflows/deploy-staging.yml
vendored
Normal 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
50
.gitignore
vendored
Normal 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
32
CHANGELOG.md
Normal 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
123
CONTRIBUTING.md
Normal 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
661
LICENSE
Normal 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
82
Makefile
Normal 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
79
README.md
Normal 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
57
SECURITY.md
Normal 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
31
bridge/.env.example
Normal 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
9
bridge/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
*.log
|
||||
.DS_Store
|
||||
.vitest/
|
||||
40
bridge/Dockerfile
Normal file
40
bridge/Dockerfile
Normal 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
43
bridge/biome.json
Normal 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
43
bridge/package.json
Normal 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
32
bridge/src/index.ts
Normal 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
41
bridge/src/lib/config.ts
Normal 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
23
bridge/src/lib/logger.ts
Normal 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
0
bridge/tests/.gitkeep
Normal file
25
bridge/tsconfig.json
Normal file
25
bridge/tsconfig.json
Normal 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
65
compose.prod.yml
Normal 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
44
compose.staging.yml
Normal 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
76
compose.yml
Normal 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:
|
||||
86
docs/01-discovery-recap.md
Normal file
86
docs/01-discovery-recap.md
Normal 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)
|
||||
207
docs/02-scope-etendu-cfa-agence.md
Normal file
207
docs/02-scope-etendu-cfa-agence.md
Normal 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
141
docs/03-decision-record.md
Normal 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
|
||||
322
docs/04-cahier-des-charges-techniques.md
Normal file
322
docs/04-cahier-des-charges-techniques.md
Normal 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
220
docs/05-data-dictionary.md
Normal 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
234
docs/06-merise-mcd.md
Normal 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
359
docs/07-merise-mld.md
Normal 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
226
docs/08-merise-mct.md
Normal 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
186
docs/09-merise-mot.md
Normal 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
202
docs/10-state-diagrams.md
Normal 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
240
docs/11-uml-use-cases.md
Normal 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)
|
||||
289
docs/12-uml-class-diagram.md
Normal file
289
docs/12-uml-class-diagram.md
Normal 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**.
|
||||
192
docs/13-uml-activity-diagrams.md
Normal file
192
docs/13-uml-activity-diagrams.md
Normal 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) |
|
||||
516
docs/14-repo-structure-gitops.md
Normal file
516
docs/14-repo-structure-gitops.md
Normal 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
428
docs/15-baserow-mpd.md
Normal 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
259
docs/16-plan-tests.md
Normal 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
500
docs/17-plan-deployment.md
Normal 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
459
docs/18-plan-operations.md
Normal 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 ?
|
||||
536
docs/19-bridge-api-design.md
Normal file
536
docs/19-bridge-api-design.md
Normal 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
20
docs/diagrams/README.md
Normal 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
|
||||
110
docs/diagrams/architecture-infra.drawio
Normal file
110
docs/diagrams/architecture-infra.drawio
Normal 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 (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 TLS Let's Encrypt 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 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 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 Node 22 + Hono (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 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 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 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 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 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 backups quotidiens 03:00 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 (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
39
scripts/backup.sh
Executable 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
48
scripts/healthcheck.sh
Executable 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
34
scripts/smoke-test.sh
Executable 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"
|
||||
Loading…
Add table
Reference in a new issue